From e43db2cbb137e01e7d43aa474d5c19d683821f64 Mon Sep 17 00:00:00 2001 From: Terry Worona Date: Sat, 26 Dec 2015 00:48:01 -0500 Subject: [PATCH] More refactors --- Classes/Line/Views/JBLineChartLinesView.h | 13 + Classes/Line/Views/JBLineChartLinesView.m | 13 + .../JBChartViewDemo.xcodeproj/project.pbxproj | 6 + .../JBChartViewDemo/Line/JBLineChartView.m | 656 +----------------- .../Line/Views/JBLineChartLinesView.h | 61 ++ .../Line/Views/JBLineChartLinesView.m | 609 ++++++++++++++++ 6 files changed, 712 insertions(+), 646 deletions(-) create mode 100644 Classes/Line/Views/JBLineChartLinesView.h create mode 100644 Classes/Line/Views/JBLineChartLinesView.m create mode 100644 JBChartViewDemo/JBChartViewDemo/Line/Views/JBLineChartLinesView.h create mode 100644 JBChartViewDemo/JBChartViewDemo/Line/Views/JBLineChartLinesView.m diff --git a/Classes/Line/Views/JBLineChartLinesView.h b/Classes/Line/Views/JBLineChartLinesView.h new file mode 100644 index 00000000..c25a4318 --- /dev/null +++ b/Classes/Line/Views/JBLineChartLinesView.h @@ -0,0 +1,13 @@ +// +// JBLineChartLinesView.h +// JBChartViewDemo +// +// Created by Terry Worona on 12/26/15. +// Copyright © 2015 Jawbone. All rights reserved. +// + +#import + +@interface JBLineChartLinesView : NSObject + +@end diff --git a/Classes/Line/Views/JBLineChartLinesView.m b/Classes/Line/Views/JBLineChartLinesView.m new file mode 100644 index 00000000..aafc6144 --- /dev/null +++ b/Classes/Line/Views/JBLineChartLinesView.m @@ -0,0 +1,13 @@ +// +// JBLineChartLinesView.m +// JBChartViewDemo +// +// Created by Terry Worona on 12/26/15. +// Copyright © 2015 Jawbone. All rights reserved. +// + +#import "JBLineChartLinesView.h" + +@implementation JBLineChartLinesView + +@end diff --git a/JBChartViewDemo/JBChartViewDemo.xcodeproj/project.pbxproj b/JBChartViewDemo/JBChartViewDemo.xcodeproj/project.pbxproj index 72f61af2..4a038795 100644 --- a/JBChartViewDemo/JBChartViewDemo.xcodeproj/project.pbxproj +++ b/JBChartViewDemo/JBChartViewDemo.xcodeproj/project.pbxproj @@ -19,6 +19,7 @@ 5683C56D1C2E4EF90017B6BA /* JBGradientLayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 5683C56C1C2E4EF90017B6BA /* JBGradientLayer.m */; }; 5683C5741C2E512C0017B6BA /* JBLineChartDotsView.m in Sources */ = {isa = PBXBuildFile; fileRef = 5683C5731C2E512C0017B6BA /* JBLineChartDotsView.m */; }; 5683C57A1C2E54100017B6BA /* JBLineChartDotView.m in Sources */ = {isa = PBXBuildFile; fileRef = 5683C5791C2E54100017B6BA /* JBLineChartDotView.m */; }; + 5683C5801C2E597D0017B6BA /* JBLineChartLinesView.m in Sources */ = {isa = PBXBuildFile; fileRef = 5683C57F1C2E597D0017B6BA /* JBLineChartLinesView.m */; }; 94BDFC3419F933B2007492F6 /* JBLineChartMissingPointsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 94BDFC3319F933B2007492F6 /* JBLineChartMissingPointsViewController.m */; }; 9B0725211829822A0052109B /* JBChartListViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 9B0725201829822A0052109B /* JBChartListViewController.m */; }; 9B2E530518218CF20079B9D2 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9B2E530418218CF20079B9D2 /* Foundation.framework */; }; @@ -78,6 +79,8 @@ 5683C5731C2E512C0017B6BA /* JBLineChartDotsView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JBLineChartDotsView.m; sourceTree = ""; }; 5683C5781C2E54100017B6BA /* JBLineChartDotView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JBLineChartDotView.h; sourceTree = ""; }; 5683C5791C2E54100017B6BA /* JBLineChartDotView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JBLineChartDotView.m; sourceTree = ""; }; + 5683C57E1C2E597D0017B6BA /* JBLineChartLinesView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JBLineChartLinesView.h; sourceTree = ""; }; + 5683C57F1C2E597D0017B6BA /* JBLineChartLinesView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JBLineChartLinesView.m; sourceTree = ""; }; 94BDFC3219F933B2007492F6 /* JBLineChartMissingPointsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JBLineChartMissingPointsViewController.h; sourceTree = ""; }; 94BDFC3319F933B2007492F6 /* JBLineChartMissingPointsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JBLineChartMissingPointsViewController.m; sourceTree = ""; }; 9B07251F1829822A0052109B /* JBChartListViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JBChartListViewController.h; sourceTree = ""; }; @@ -231,6 +234,8 @@ 5683C5731C2E512C0017B6BA /* JBLineChartDotsView.m */, 5683C5781C2E54100017B6BA /* JBLineChartDotView.h */, 5683C5791C2E54100017B6BA /* JBLineChartDotView.m */, + 5683C57E1C2E597D0017B6BA /* JBLineChartLinesView.h */, + 5683C57F1C2E597D0017B6BA /* JBLineChartLinesView.m */, ); path = Views; sourceTree = ""; @@ -525,6 +530,7 @@ buildActionMask = 2147483647; files = ( 9BE0B0CE182B162E00232023 /* JBBarChartFooterView.m in Sources */, + 5683C5801C2E597D0017B6BA /* JBLineChartLinesView.m in Sources */, 5683C5611C2E4D720017B6BA /* JBBarChartView.m in Sources */, 9BEBE9D2183167050046E4A8 /* JBChartInformationView.m in Sources */, 5683C5651C2E4D720017B6BA /* JBShapeLayer.m in Sources */, diff --git a/JBChartViewDemo/JBChartViewDemo/Line/JBLineChartView.m b/JBChartViewDemo/JBChartViewDemo/Line/JBLineChartView.m index 7cc41d63..24724606 100755 --- a/JBChartViewDemo/JBChartViewDemo/Line/JBLineChartView.m +++ b/JBChartViewDemo/JBChartViewDemo/Line/JBLineChartView.m @@ -21,6 +21,7 @@ // Views #import "JBLineChartDotsView.h" +#import "JBLineChartLinesView.h" // Enums typedef NS_ENUM(NSUInteger, JBLineChartHorizontalIndexClamp){ @@ -29,18 +30,6 @@ typedef NS_ENUM(NSUInteger, JBLineChartHorizontalIndexClamp){ JBLineChartHorizontalIndexClampNone }; -// Numerics (JBLineChartLineView) -CGFloat const kJBLineChartLinesViewStrokeWidth = 5.0; -CGFloat const kJBLineChartLinesViewMiterLimit = -5.0; -CGFloat const kJBLineChartLinesViewDefaultLinePhase = 1.0f; -CGFloat const kJBLineChartLinesViewDefaultDimmedOpacity = 0.20f; -CGFloat const kJBLineChartLinesViewSmoothThresholdSlope = 0.01f; -CGFloat const kJBLineChartLinesViewReloadDataAnimationDuration = 0.15f; -NSInteger const kJBLineChartLinesViewDefaultDotRadiusFactor = 3; // 3x size of line width -NSInteger const kJBLineChartLinesViewSmoothThresholdVertical = 1; -NSInteger const kJBLineChartLinesViewUnselectedLineIndex = -1; -static NSArray *kJBLineChartLinesViewDefaultDashPattern = nil; - // Numerics (JBLineSelectionView) CGFloat const kJBLineSelectionViewWidth = 20.0f; @@ -52,6 +41,9 @@ CGFloat const kJBLineChartViewStateBounceOffset = 15.0f; CGFloat const kJBLineChartViewDefaultStartPoint = 0.0; CGFloat const kJBLineChartViewDefaultEndPoint = 1.0; CGFloat const kJBLineChartViewReloadAnimationDuration = 0.1; +CGFloat const kJBLineChartViewDefaultDimmedSelectionOpacity = 0.20f; +CGFloat const kJBLineChartViewDefaultStrokeWidth = 5.0f; +NSInteger const kJBLineChartViewDefaultDotRadiusFactor = 3; // 3x size of line width NSInteger const kJBLineChartUnselectedLineIndex = -1; // Colors (JBLineChartView) @@ -70,48 +62,6 @@ static UIColor *kJBLineChartViewDefaultFillGradientEndColor = nil; @end -@protocol JBLineChartLinesViewDelegate; - -@interface JBLineChartLinesView : UIView - -@property (nonatomic, assign) id delegate; -@property (nonatomic, assign) NSInteger selectedLineIndex; // -1 to unselect -@property (nonatomic, assign) BOOL animated; // for reload - -// Data -- (void)reloadDataAnimated:(BOOL)animated callback:(void (^)())callback; -- (void)reloadDataAnimated:(BOOL)animated; -- (void)reloadData; - -// Setters -- (void)setSelectedLineIndex:(NSInteger)selectedLineIndex animated:(BOOL)animated; - -// Getters -- (UIBezierPath *)bezierPathForLineChartLine:(JBLineChartLine *)lineChartLine filled:(BOOL)filled; -- (JBShapeLayer *)shapeLayerForLineIndex:(NSUInteger)lineIndex filled:(BOOL)filled; -- (JBGradientLayer *)gradientLayerForLineIndex:(NSUInteger)lineIndex filled:(BOOL)filled; - -// Callback helpers -- (void)fireCallback:(void (^)())callback; - -@end - -@protocol JBLineChartLinesViewDelegate - -- (NSArray *)lineChartLinesForLineChartLinesView:(JBLineChartLinesView *)lineChartLinesView; -- (CGFloat)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView dimmedSelectionOpacityAtLineIndex:(NSUInteger)lineIndex; -- (UIColor *)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView colorForLineAtLineIndex:(NSUInteger)lineIndex; -- (CAGradientLayer *)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView gradientForLineAtLineIndex:(NSUInteger)lineIndex; -- (UIColor *)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView fillColorForLineAtLineIndex:(NSUInteger)lineIndex; -- (CAGradientLayer *)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView fillGradientForLineAtLineIndex:(NSUInteger)lineIndex; -- (CGFloat)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView widthForLineAtLineIndex:(NSUInteger)lineIndex; -- (UIColor *)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView selectionColorForLineAtLineIndex:(NSUInteger)lineIndex; -- (CAGradientLayer *)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView selectionGradientForLineAtLineIndex:(NSUInteger)lineIndex; -- (UIColor *)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView selectionFillColorForLineAtLineIndex:(NSUInteger)lineIndex; -- (CAGradientLayer *)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView selectionFillGradientForLineAtLineIndex:(NSUInteger)lineIndex; - -@end - @interface JBLineChartView () @property (nonatomic, strong) NSArray *lineChartLines; // Collection of JBLineChartLines @@ -527,7 +477,7 @@ static UIColor *kJBLineChartViewDefaultFillGradientEndColor = nil; showsDots = [self.dataSource lineChartView:self showsDotsForLineAtLineIndex:lineIndex]; } - CGFloat lineWidth = kJBLineChartLinesViewStrokeWidth; // default + CGFloat lineWidth = kJBLineChartViewDefaultStrokeWidth; // default if ([self.delegate respondsToSelector:@selector(lineChartView:widthForLineAtLineIndex:)]) { lineWidth = [self.delegate lineChartView:self widthForLineAtLineIndex:lineIndex]; @@ -594,7 +544,7 @@ static UIColor *kJBLineChartViewDefaultFillGradientEndColor = nil; } else { - CGFloat defaultDotRadius = ((lineWidth * kJBLineChartLinesViewDefaultDotRadiusFactor) * 2.0f); + CGFloat defaultDotRadius = ((lineWidth * kJBLineChartViewDefaultDotRadiusFactor) * 2.0f); if (defaultDotRadius > maxDotLength) { maxDotLength = defaultDotRadius; @@ -643,7 +593,7 @@ static UIColor *kJBLineChartViewDefaultFillGradientEndColor = nil; { return [self.dataSource lineChartView:self dimmedSelectionOpacityAtLineIndex:lineIndex]; } - return kJBLineChartLinesViewDefaultDimmedOpacity; + return kJBLineChartViewDefaultDimmedSelectionOpacity; } - (UIColor *)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView colorForLineAtLineIndex:(NSUInteger)lineIndex @@ -688,7 +638,7 @@ static UIColor *kJBLineChartViewDefaultFillGradientEndColor = nil; { return [self.delegate lineChartView:self widthForLineAtLineIndex:lineIndex]; } - return kJBLineChartLinesViewStrokeWidth; + return kJBLineChartViewDefaultStrokeWidth; } - (UIColor *)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView selectionColorForLineAtLineIndex:(NSUInteger)lineIndex @@ -758,7 +708,7 @@ static UIColor *kJBLineChartViewDefaultFillGradientEndColor = nil; { return [self.delegate lineChartView:self widthForLineAtLineIndex:lineIndex]; } - return kJBLineChartLinesViewStrokeWidth; + return kJBLineChartViewDefaultStrokeWidth; } - (CGFloat)lineChartDotsView:(JBLineChartDotsView *)lineChartDotsView dotRadiusForLineAtHorizontalIndex:(NSUInteger)horizontalIndex atLineIndex:(NSUInteger)lineIndex @@ -767,7 +717,7 @@ static UIColor *kJBLineChartViewDefaultFillGradientEndColor = nil; { return [self.delegate lineChartView:self dotRadiusForDotAtHorizontalIndex:horizontalIndex atLineIndex:lineIndex]; } - return [self lineChartDotsView:lineChartDotsView widthForLineAtLineIndex:lineIndex] * kJBLineChartLinesViewDefaultDotRadiusFactor; + return [self lineChartDotsView:lineChartDotsView widthForLineAtLineIndex:lineIndex] * kJBLineChartViewDefaultDotRadiusFactor; } - (UIView *)lineChartDotsView:(JBLineChartDotsView *)lineChartDotsView dotViewAtHorizontalIndex:(NSUInteger)horizontalIndex atLineIndex:(NSUInteger)lineIndex @@ -1208,589 +1158,3 @@ static UIColor *kJBLineChartViewDefaultFillGradientEndColor = nil; } @end - -@implementation JBLineChartLinesView - -#pragma mark - Alloc/Init - -+ (void)initialize -{ - if (self == [JBLineChartLinesView class]) - { - kJBLineChartLinesViewDefaultDashPattern = @[@(3), @(2)]; - } -} - -- (id)initWithFrame:(CGRect)frame -{ - self = [super initWithFrame:frame]; - if (self) - { - self.backgroundColor = [UIColor clearColor]; - } - return self; -} - -#pragma mark - Memory Management - -- (void)dealloc -{ - [NSObject cancelPreviousPerformRequestsWithTarget:self]; -} - -#pragma mark - Drawing - -- (void)drawRect:(CGRect)rect -{ - [super drawRect:rect]; - - NSAssert([self.delegate respondsToSelector:@selector(lineChartLinesForLineChartLinesView:)], @"JBLineChartLinesView // delegate must implement - (NSArray *)lineChartLinesForLineChartLinesView:(JBLineChartLinesView *)lineChartLinesView"); - NSArray *chartData = [self.delegate lineChartLinesForLineChartLinesView:self]; - - for (int lineIndex=0; lineIndex<[chartData count]; lineIndex++) - { - JBLineChartLine *lineChartLine = [chartData objectAtIndex:lineIndex]; - { - UIBezierPath *linePath = [self bezierPathForLineChartLine:lineChartLine filled:NO]; - UIBezierPath *fillPath = [self bezierPathForLineChartLine:lineChartLine filled:YES]; - - if (linePath == nil || fillPath == nil) - { - continue; - } - - JBShapeLayer *shapeLayer = [self shapeLayerForLineIndex:lineIndex filled:NO]; - if (shapeLayer == nil) - { - shapeLayer = [[JBShapeLayer alloc] initWithTag:lineIndex filled:NO currentPath:linePath]; - } - - JBShapeLayer *fillLayer = [self shapeLayerForLineIndex:lineIndex filled:YES]; - if (fillLayer == nil) - { - fillLayer = [[JBShapeLayer alloc] initWithTag:lineIndex filled:YES currentPath:nil]; // don't need currentPath since fill's aren't animatable (yet) - } - - shapeLayer.zPosition = 0.1f; - shapeLayer.fillColor = [UIColor clearColor].CGColor; - fillLayer.zPosition = 0.1f; - fillLayer.fillColor = [UIColor clearColor].CGColor; - - // Line style - if (lineChartLine.lineStyle == JBLineChartViewLineStyleSolid) - { - shapeLayer.lineDashPhase = 0.0; - shapeLayer.lineDashPattern = nil; - } - else if (lineChartLine.lineStyle == JBLineChartViewLineStyleDashed) - { - shapeLayer.lineDashPhase = kJBLineChartLinesViewDefaultLinePhase; - shapeLayer.lineDashPattern = kJBLineChartLinesViewDefaultDashPattern; - } - - // Smoothing - if (lineChartLine.smoothedLine) - { - if (lineChartLine.lineStyle == JBLineChartViewLineStyleDashed) - { - shapeLayer.lineCap = kCALineCapButt; // smoothed, dashed lines need butt caps - } - else - { - shapeLayer.lineCap = kCALineCapRound; - } - shapeLayer.lineJoin = kCALineJoinRound; - fillLayer.lineCap = kCALineCapRound; - fillLayer.lineJoin = kCALineJoinRound; - } - else - { - shapeLayer.lineCap = kCALineCapButt; - shapeLayer.lineJoin = kCALineJoinMiter; - fillLayer.lineCap = kCALineCapButt; - fillLayer.lineJoin = kCALineJoinMiter; - } - - // Width - NSAssert([self.delegate respondsToSelector:@selector(lineChartLinesView:widthForLineAtLineIndex:)], @"JBLineChartLinesView // delegate must implement - (CGFloat)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView widthForLineAtLineIndex:(NSUInteger)lineIndex"); - shapeLayer.lineWidth = [self.delegate lineChartLinesView:self widthForLineAtLineIndex:lineIndex]; - fillLayer.lineWidth = [self.delegate lineChartLinesView:self widthForLineAtLineIndex:lineIndex]; - - // Colors - NSAssert([self.delegate respondsToSelector:@selector(lineChartLinesView:colorForLineAtLineIndex:)], @"JBLineChartLinesView // delegate must implement - (UIColor *)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView colorForLineAtLineIndex:(NSUInteger)lineIndex"); - shapeLayer.strokeColor = [self.delegate lineChartLinesView:self colorForLineAtLineIndex:lineIndex].CGColor; - - // Line path - shapeLayer.frame = self.bounds; - if (self.animated) - { - CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:@"path"]; - pathAnimation.fromValue = (id)shapeLayer.currentPath.CGPath; - pathAnimation.toValue = (id)linePath.CGPath; - pathAnimation.duration = kJBLineChartLinesViewReloadDataAnimationDuration; - pathAnimation.timingFunction = [CAMediaTimingFunction functionWithName:@"easeInEaseOut"]; - pathAnimation.fillMode = kCAFillModeBoth; - pathAnimation.removedOnCompletion = NO; - [shapeLayer addAnimation:pathAnimation forKey:@"shapeLayerPathAnimation"]; - } - else - { - shapeLayer.path = linePath.CGPath; - } - shapeLayer.currentPath = [linePath copy]; - - // Fill path - fillLayer.frame = self.bounds; - fillLayer.path = fillPath.CGPath; - - // Solid fill - if (lineChartLine.fillColorStyle == JBLineChartViewColorStyleSolid) - { - NSAssert([self.delegate respondsToSelector:@selector(lineChartLinesView:fillColorForLineAtLineIndex:)], @"JBLineChartLinesView // delegate must implement - (UIColor *)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView fillColorForLineAtLineIndex:(NSUInteger)lineIndex"); - fillLayer.fillColor = [self.delegate lineChartLinesView:self fillColorForLineAtLineIndex:lineIndex].CGColor; - [self.layer addSublayer:fillLayer]; - } - - // Gradient fill - else if (lineChartLine.fillColorStyle == JBLineChartViewColorStyleGradient) - { - JBGradientLayer *fillGradientLayer = [self gradientLayerForLineIndex:lineIndex filled:YES]; - if (fillGradientLayer == nil) - { - NSAssert([self.delegate respondsToSelector:@selector(lineChartLinesView:fillGradientForLineAtLineIndex:)], @"JBLineChartLinesView // delegate must implement - (CAGradientLayer *)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView fillGradientForLineAtLineIndex:(NSUInteger)lineIndex"); - fillGradientLayer = [[JBGradientLayer alloc] initWithGradientLayer:[self.delegate lineChartLinesView:self fillGradientForLineAtLineIndex:lineIndex] tag:lineIndex filled:YES currentPath:nil]; - } - fillGradientLayer.frame = fillLayer.frame; - fillGradientLayer.mask = fillLayer; - [self.layer addSublayer:fillGradientLayer]; - } - - // Solid line - if (lineChartLine.colorStyle == JBLineChartViewColorStyleSolid) - { - [self.layer addSublayer:shapeLayer]; - } - - // Gradient line - else if (lineChartLine.colorStyle == JBLineChartViewColorStyleGradient) - { - JBGradientLayer *gradientLayer = [self gradientLayerForLineIndex:lineIndex filled:NO]; - if (gradientLayer == nil) - { - NSAssert([self.delegate respondsToSelector:@selector(lineChartLinesView:gradientForLineAtLineIndex:)], @"JBLineChartLinesView // delegate must implement - (CAGradientLayer *)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView gradientForLineAtLineIndex:(NSUInteger)lineIndex"); - gradientLayer = [[JBGradientLayer alloc] initWithGradientLayer:[self.delegate lineChartLinesView:self gradientForLineAtLineIndex:lineIndex] tag:lineIndex filled:NO currentPath:linePath]; - } - gradientLayer.frame = shapeLayer.frame; - - if (self.animated) - { - CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:@"path"]; - pathAnimation.fromValue = (id)gradientLayer.currentPath.CGPath; - pathAnimation.toValue = (id)linePath.CGPath; - pathAnimation.duration = kJBLineChartLinesViewReloadDataAnimationDuration; - pathAnimation.timingFunction = [CAMediaTimingFunction functionWithName:@"easeInEaseOut"]; - pathAnimation.fillMode = kCAFillModeBoth; - pathAnimation.removedOnCompletion = NO; - [gradientLayer.mask addAnimation:pathAnimation forKey:@"gradientLayerMaskAnimation"]; - } - else - { - gradientLayer.mask = shapeLayer; - } - gradientLayer.currentPath = [linePath copy]; - - [self.layer addSublayer:gradientLayer]; - } - } - } - - self.animated = NO; -} - -#pragma mark - Data - -- (void)reloadDataAnimated:(BOOL)animated callback:(void (^)())callback -{ - NSAssert([self.delegate respondsToSelector:@selector(lineChartLinesForLineChartLinesView:)], @"JBLineChartLinesView // delegate must implement - (NSArray *)lineChartLinesForLineChartLinesView:(JBLineChartLinesView *)lineChartLinesView"); - NSArray *chartData = [self.delegate lineChartLinesForLineChartLinesView:self]; - - NSUInteger lineCount = [chartData count]; - - __weak JBLineChartLinesView* weakSelf = self; - - dispatch_block_t completionBlock = ^{ - weakSelf.animated = NO; - [weakSelf setNeedsDisplay]; // re-draw layers - if (callback) - { - callback(); - } - }; - - // Mark layers for animation or removal - NSMutableArray *mutableRemovedLayers = [NSMutableArray array]; - for (CALayer *layer in [self.layer sublayers]) - { - BOOL removeLayer = NO; - - if ([layer isKindOfClass:[JBShapeLayer class]]) - { - removeLayer = (((JBShapeLayer *)layer).tag >= lineCount); - } - else if ([layer isKindOfClass:[JBGradientLayer class]]) - { - removeLayer = (((JBGradientLayer *)layer).tag >= lineCount); - } - - if (removeLayer) - { - [mutableRemovedLayers addObject:layer]; - } - } - - // Remove legacy layers - NSArray *removedLayers = [NSArray arrayWithArray:mutableRemovedLayers]; - if ([removedLayers count] > 0) - { - for (int index=0; index<[removedLayers count]; index++) - { - CALayer *removedLayer = [removedLayers objectAtIndex:index]; - - if (animated) - { - [CATransaction begin]; - { - CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"opacity"]; - animation.fromValue = [NSNumber numberWithFloat:1.0f]; - animation.toValue = [NSNumber numberWithFloat:0.0f]; - animation.duration = kJBLineChartLinesViewReloadDataAnimationDuration; - animation.timingFunction = [CAMediaTimingFunction functionWithName:@"easeInEaseOut"]; - animation.fillMode = kCAFillModeBoth; - animation.removedOnCompletion = NO; - - [CATransaction setCompletionBlock:^{ - [removedLayer removeFromSuperlayer]; - if (index == [removedLayers count]-1) - { - completionBlock(); - } - }]; - - [removedLayer addAnimation:animation forKey:@"removeShapeLayerAnimation"]; - } - [CATransaction commit]; - } - else - { - [removedLayer removeFromSuperlayer]; - if (index == [removedLayers count]-1) - { - completionBlock(); - } - } - } - } - else - { - completionBlock(); - } -} - -- (void)reloadDataAnimated:(BOOL)animated -{ - [self reloadDataAnimated:animated callback:nil]; -} - -- (void)reloadData -{ - [self reloadDataAnimated:NO]; -} - -#pragma mark - Setters - -- (void)setSelectedLineIndex:(NSInteger)selectedLineIndex animated:(BOOL)animated -{ - _selectedLineIndex = selectedLineIndex; - - __weak JBLineChartLinesView* weakSelf = self; - - dispatch_block_t adjustLines = ^{ - NSMutableArray *layersToReplace = [NSMutableArray array]; - - NSString * const oldLayerKey = @"oldLayer"; - NSString * const newLayerKey = @"newLayer"; - - for (CALayer *layer in [weakSelf.layer sublayers]) - { - /* - * Solid line or fill - */ - if ([layer isKindOfClass:[JBShapeLayer class]]) - { - JBShapeLayer *shapeLayer = (JBShapeLayer * )layer; - - if (shapeLayer.filled) - { - // Selected solid fill - if (shapeLayer.tag == weakSelf.selectedLineIndex) - { - NSAssert([self.delegate respondsToSelector:@selector(lineChartLinesView:selectionFillColorForLineAtLineIndex:)], @"JBLineChartLinesView // delegate must implement - (UIColor *)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView selectionFillColorForLineAtLineIndex:(NSUInteger)lineIndex"); - shapeLayer.fillColor = [self.delegate lineChartLinesView:self selectionFillColorForLineAtLineIndex:shapeLayer.tag].CGColor; - shapeLayer.opacity = 1.0f; - } - // Unselected solid fill - else - { - NSAssert([self.delegate respondsToSelector:@selector(lineChartLinesView:fillColorForLineAtLineIndex:)], @"JBLineChartLinesView // delegate must implement - (UIColor *)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView fillColorForLineAtLineIndex:(NSUInteger)lineIndex"); - shapeLayer.fillColor = [self.delegate lineChartLinesView:self fillColorForLineAtLineIndex:shapeLayer.tag].CGColor; - - NSAssert([self.delegate respondsToSelector:@selector(lineChartLinesView:dimmedSelectionOpacityAtLineIndex:)], @"JBLineChartLinesView // delegate must implement - (CGFloat)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView dimmedSelectionOpacityAtLineIndex:(NSUInteger)lineIndex"); - shapeLayer.opacity = (weakSelf.selectedLineIndex == kJBLineChartLinesViewUnselectedLineIndex) ? 1.0f : [self.delegate lineChartLinesView:self dimmedSelectionOpacityAtLineIndex:shapeLayer.tag]; - } - } - else - { - // Selected solid line - if (shapeLayer.tag == weakSelf.selectedLineIndex) - { - NSAssert([self.delegate respondsToSelector:@selector(lineChartLinesView:selectionColorForLineAtLineIndex:)], @"JBLineChartLinesView // delegate must implement - (UIColor *)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView selectionColorForLineAtLineIndex:(NSUInteger)lineIndex"); - shapeLayer.strokeColor = [self.delegate lineChartLinesView:self selectionColorForLineAtLineIndex:shapeLayer.tag].CGColor; - shapeLayer.opacity = 1.0f; - } - // Unselected solid line - else - { - NSAssert([self.delegate respondsToSelector:@selector(lineChartLinesView:colorForLineAtLineIndex:)], @"JBLineChartLinesView // delegate must implement - (UIColor *)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView colorForLineAtLineIndex:(NSUInteger)lineIndex"); - shapeLayer.strokeColor = [self.delegate lineChartLinesView:self colorForLineAtLineIndex:shapeLayer.tag].CGColor; - - NSAssert([self.delegate respondsToSelector:@selector(lineChartLinesView:dimmedSelectionOpacityAtLineIndex:)], @"JBLineChartLinesView // delegate must implement - (CGFloat)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView dimmedSelectionOpacityAtLineIndex:(NSUInteger)lineIndex"); - shapeLayer.opacity = (weakSelf.selectedLineIndex == kJBLineChartLinesViewUnselectedLineIndex) ? 1.0f : [self.delegate lineChartLinesView:self dimmedSelectionOpacityAtLineIndex:shapeLayer.tag]; - } - } - } - - /* - * Gradient line or fill - */ - else if ([layer isKindOfClass:[CAGradientLayer class]]) - { - CAGradientLayer *gradientLayer = (CAGradientLayer * )layer; - - if ([gradientLayer.mask isKindOfClass:[JBShapeLayer class]]) - { - JBShapeLayer *shapeLayer = (JBShapeLayer * )gradientLayer.mask; - - if (shapeLayer.filled) - { - // Selected gradient fill - if (shapeLayer.tag == weakSelf.selectedLineIndex) - { - NSAssert([self.delegate respondsToSelector:@selector(lineChartLinesView:selectionFillGradientForLineAtLineIndex:)], @"JBLineChartLinesView // delegate must implement - (CAGradientLayer *)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView selectionFillGradientForLineAtLineIndex:(NSUInteger)lineIndex"); - CAGradientLayer *selectedFillGradient = [self.delegate lineChartLinesView:self selectionFillGradientForLineAtLineIndex:shapeLayer.tag]; - selectedFillGradient.frame = layer.frame; - selectedFillGradient.mask = layer.mask; - selectedFillGradient.opacity = 1.0f; - [layersToReplace addObject:@{oldLayerKey: layer, newLayerKey: selectedFillGradient}]; - } - // Unselected gradient fill - else - { - NSAssert([self.delegate respondsToSelector:@selector(lineChartLinesView:fillGradientForLineAtLineIndex:)], @"JBLineChartLinesView // delegate must implement - (CAGradientLayer *)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView fillGradientForLineAtLineIndex:(NSUInteger)lineIndex"); - CAGradientLayer *unselectedFillGradient = [self.delegate lineChartLinesView:self fillGradientForLineAtLineIndex:shapeLayer.tag]; - unselectedFillGradient.frame = layer.frame; - unselectedFillGradient.mask = layer.mask; - NSAssert([self.delegate respondsToSelector:@selector(lineChartLinesView:dimmedSelectionOpacityAtLineIndex:)], @"JBLineChartLinesView // delegate must implement - (CGFloat)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView dimmedSelectionOpacityAtLineIndex:(NSUInteger)lineIndex"); - unselectedFillGradient.opacity = (weakSelf.selectedLineIndex == kJBLineChartLinesViewUnselectedLineIndex) ? 1.0f : [self.delegate lineChartLinesView:self dimmedSelectionOpacityAtLineIndex:shapeLayer.tag]; - [layersToReplace addObject:@{oldLayerKey: layer, newLayerKey: unselectedFillGradient}]; - } - } - else - { - // Selected gradient line - if (shapeLayer.tag == weakSelf.selectedLineIndex) - { - NSAssert([self.delegate respondsToSelector:@selector(lineChartLinesView:selectionGradientForLineAtLineIndex:)], @"JBLineChartLinesView // delegate must implement - (CAGradientLayer *)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView selectionGradientForLineAtLineIndex:(NSUInteger)lineIndex"); - CAGradientLayer *selectedGradient = [self.delegate lineChartLinesView:self selectionGradientForLineAtLineIndex:shapeLayer.tag]; - selectedGradient.frame = layer.frame; - selectedGradient.mask = layer.mask; - selectedGradient.opacity = 1.0f; - [layersToReplace addObject:@{oldLayerKey: layer, newLayerKey: selectedGradient}]; - } - // Unselected gradient line - else - { - NSAssert([self.delegate respondsToSelector:@selector(lineChartLinesView:gradientForLineAtLineIndex:)], @"JBLineChartLinesView // delegate must implement - (CAGradientLayer *)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView gradientForLineAtLineIndex:(NSUInteger)lineIndex"); - CAGradientLayer *unselectedGradient = [self.delegate lineChartLinesView:self gradientForLineAtLineIndex:shapeLayer.tag]; - unselectedGradient.frame = layer.frame; - unselectedGradient.mask = layer.mask; - NSAssert([self.delegate respondsToSelector:@selector(lineChartLinesView:dimmedSelectionOpacityAtLineIndex:)], @"JBLineChartLinesView // delegate must implement - (CGFloat)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView dimmedSelectionOpacityAtLineIndex:(NSUInteger)lineIndex"); - shapeLayer.opacity = (weakSelf.selectedLineIndex == kJBLineChartLinesViewUnselectedLineIndex) ? 1.0f : [self.delegate lineChartLinesView:self dimmedSelectionOpacityAtLineIndex:shapeLayer.tag]; - [layersToReplace addObject:@{oldLayerKey: layer, newLayerKey: unselectedGradient}]; - } - } - } - } - } - - for (NSDictionary *layerPair in layersToReplace) - { - [weakSelf.layer replaceSublayer:layerPair[oldLayerKey] with:layerPair[newLayerKey]]; - } - }; - - if (animated) - { - [UIView animateWithDuration:kJBChartViewDefaultAnimationDuration animations:^{ - adjustLines(); - }]; - } - else - { - adjustLines(); - } -} - -- (void)setSelectedLineIndex:(NSInteger)selectedLineIndex -{ - [self setSelectedLineIndex:selectedLineIndex animated:NO]; -} - -#pragma mark - Getters - -- (UIBezierPath *)bezierPathForLineChartLine:(JBLineChartLine *)lineChartLine filled:(BOOL)filled -{ - if ([lineChartLine.lineChartPoints count] > 0) - { - UIBezierPath *bezierPath = [UIBezierPath bezierPath]; - - bezierPath.miterLimit = kJBLineChartLinesViewMiterLimit; - - JBLineChartPoint *previousLineChartPoint = nil; - CGFloat previousSlope = 0.0f; - - BOOL visiblePointFound = NO; - NSArray *sortedLineChartPoints = [lineChartLine.lineChartPoints sortedArrayUsingSelector:@selector(compare:)]; - CGFloat firstXPosition = 0.0f; - CGFloat firstYPosition = 0.0f; - CGFloat lastXPosition = 0.0f; - CGFloat lastYPosition = 0.0f; - - for (NSUInteger index=0; index<[sortedLineChartPoints count]; index++) - { - JBLineChartPoint *lineChartPoint = [sortedLineChartPoints objectAtIndex:index]; - - if (lineChartPoint.hidden) - { - continue; - } - - if (!visiblePointFound) - { - [bezierPath moveToPoint:CGPointMake(lineChartPoint.position.x, lineChartPoint.position.y)]; - firstXPosition = lineChartPoint.position.x; - firstYPosition = lineChartPoint.position.y; - visiblePointFound = YES; - } - else - { - JBLineChartPoint *nextLineChartPoint = nil; - if (index != ([lineChartLine.lineChartPoints count] - 1)) - { - nextLineChartPoint = [sortedLineChartPoints objectAtIndex:(index + 1)]; - } - - CGFloat nextSlope = (nextLineChartPoint != nil) ? ((nextLineChartPoint.position.y - lineChartPoint.position.y)) / ((nextLineChartPoint.position.x - lineChartPoint.position.x)) : previousSlope; - CGFloat currentSlope = ((lineChartPoint.position.y - previousLineChartPoint.position.y)) / (lineChartPoint.position.x-previousLineChartPoint.position.x); - - BOOL deltaFromNextSlope = ((currentSlope >= (nextSlope + kJBLineChartLinesViewSmoothThresholdSlope)) || (currentSlope <= (nextSlope - kJBLineChartLinesViewSmoothThresholdSlope))); - BOOL deltaFromPreviousSlope = ((currentSlope >= (previousSlope + kJBLineChartLinesViewSmoothThresholdSlope)) || (currentSlope <= (previousSlope - kJBLineChartLinesViewSmoothThresholdSlope))); - BOOL deltaFromPreviousY = (lineChartPoint.position.y >= previousLineChartPoint.position.y + kJBLineChartLinesViewSmoothThresholdVertical) || (lineChartPoint.position.y <= previousLineChartPoint.position.y - kJBLineChartLinesViewSmoothThresholdVertical); - - if (lineChartLine.smoothedLine && deltaFromNextSlope && deltaFromPreviousSlope && deltaFromPreviousY) - { - CGFloat deltaX = lineChartPoint.position.x - previousLineChartPoint.position.x; - CGFloat controlPointX = previousLineChartPoint.position.x + (deltaX / 2); - - CGPoint controlPoint1 = CGPointMake(controlPointX, previousLineChartPoint.position.y); - CGPoint controlPoint2 = CGPointMake(controlPointX, lineChartPoint.position.y); - - [bezierPath addCurveToPoint:CGPointMake(lineChartPoint.position.x, lineChartPoint.position.y) controlPoint1:controlPoint1 controlPoint2:controlPoint2]; - } - else - { - [bezierPath addLineToPoint:CGPointMake(lineChartPoint.position.x, lineChartPoint.position.y)]; - } - - lastXPosition = lineChartPoint.position.x; - lastYPosition = lineChartPoint.position.y; - previousSlope = currentSlope; - } - previousLineChartPoint = lineChartPoint; - } - - if (filled) - { - UIBezierPath *filledBezierPath = [bezierPath copy]; - - if(visiblePointFound) - { - [filledBezierPath addLineToPoint:CGPointMake(lastXPosition, lastYPosition)]; - [filledBezierPath addLineToPoint:CGPointMake(lastXPosition, self.bounds.size.height)]; - - [filledBezierPath addLineToPoint:CGPointMake(firstXPosition, self.bounds.size.height)]; - [filledBezierPath addLineToPoint:CGPointMake(firstXPosition, firstYPosition)]; - } - - return filledBezierPath; - } - else - { - return bezierPath; - } - } - return nil; -} - -- (JBShapeLayer *)shapeLayerForLineIndex:(NSUInteger)lineIndex filled:(BOOL)filled -{ - for (CALayer *layer in [self.layer sublayers]) - { - if ([layer isKindOfClass:[JBShapeLayer class]]) - { - if (((JBShapeLayer *)layer).tag == lineIndex && ((JBShapeLayer *)layer).filled == filled) - { - return (JBShapeLayer *)layer; - } - } - } - return nil; -} - -- (JBGradientLayer *)gradientLayerForLineIndex:(NSUInteger)lineIndex filled:(BOOL)filled -{ - for (CALayer *layer in [self.layer sublayers]) - { - if ([layer isKindOfClass:[JBGradientLayer class]]) - { - if (((JBGradientLayer *)layer).tag == lineIndex && ((JBGradientLayer *)layer).filled == filled) - { - return (JBGradientLayer *)layer; - } - } - } - return nil; -} - -#pragma mark - Callback Helpers - -- (void)fireCallback:(void (^)())callback -{ - dispatch_block_t callbackCopy = [callback copy]; - - if (callbackCopy != nil) - { - callbackCopy(); - } -} - -@end diff --git a/JBChartViewDemo/JBChartViewDemo/Line/Views/JBLineChartLinesView.h b/JBChartViewDemo/JBChartViewDemo/Line/Views/JBLineChartLinesView.h new file mode 100644 index 00000000..b032a0a8 --- /dev/null +++ b/JBChartViewDemo/JBChartViewDemo/Line/Views/JBLineChartLinesView.h @@ -0,0 +1,61 @@ +// +// JBLineChartLinesView.h +// JBChartViewDemo +// +// Created by Terry Worona on 12/26/15. +// Copyright © 2015 Jawbone. All rights reserved. +// + +#import + +// Layers +#import "JBShapeLayer.h" +#import "JBGradientLayer.h" + +// Models +#import "JBLineChartLine.h" + +// Numerics +extern NSInteger const kJBLineChartLinesViewUnselectedLineIndex; + +@protocol JBLineChartLinesViewDelegate; + +@interface JBLineChartLinesView : UIView + +@property (nonatomic, assign) id delegate; +@property (nonatomic, assign) NSInteger selectedLineIndex; // -1 to unselect +@property (nonatomic, assign) BOOL animated; // for reload + +// Data +- (void)reloadDataAnimated:(BOOL)animated callback:(void (^)())callback; +- (void)reloadDataAnimated:(BOOL)animated; +- (void)reloadData; + +// Setters +- (void)setSelectedLineIndex:(NSInteger)selectedLineIndex animated:(BOOL)animated; + +// Getters +- (UIBezierPath *)bezierPathForLineChartLine:(JBLineChartLine *)lineChartLine filled:(BOOL)filled; +- (JBShapeLayer *)shapeLayerForLineIndex:(NSUInteger)lineIndex filled:(BOOL)filled; +- (JBGradientLayer *)gradientLayerForLineIndex:(NSUInteger)lineIndex filled:(BOOL)filled; + +// Callback helpers +- (void)fireCallback:(void (^)())callback; + +@end + +@protocol JBLineChartLinesViewDelegate + +- (NSArray *)lineChartLinesForLineChartLinesView:(JBLineChartLinesView *)lineChartLinesView; +- (CGFloat)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView dimmedSelectionOpacityAtLineIndex:(NSUInteger)lineIndex; +- (UIColor *)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView colorForLineAtLineIndex:(NSUInteger)lineIndex; +- (CAGradientLayer *)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView gradientForLineAtLineIndex:(NSUInteger)lineIndex; +- (UIColor *)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView fillColorForLineAtLineIndex:(NSUInteger)lineIndex; +- (CAGradientLayer *)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView fillGradientForLineAtLineIndex:(NSUInteger)lineIndex; +- (CGFloat)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView widthForLineAtLineIndex:(NSUInteger)lineIndex; +- (UIColor *)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView selectionColorForLineAtLineIndex:(NSUInteger)lineIndex; +- (CAGradientLayer *)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView selectionGradientForLineAtLineIndex:(NSUInteger)lineIndex; +- (UIColor *)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView selectionFillColorForLineAtLineIndex:(NSUInteger)lineIndex; +- (CAGradientLayer *)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView selectionFillGradientForLineAtLineIndex:(NSUInteger)lineIndex; + +@end diff --git a/JBChartViewDemo/JBChartViewDemo/Line/Views/JBLineChartLinesView.m b/JBChartViewDemo/JBChartViewDemo/Line/Views/JBLineChartLinesView.m new file mode 100644 index 00000000..ab341015 --- /dev/null +++ b/JBChartViewDemo/JBChartViewDemo/Line/Views/JBLineChartLinesView.m @@ -0,0 +1,609 @@ +// +// JBLineChartLinesView.m +// JBChartViewDemo +// +// Created by Terry Worona on 12/26/15. +// Copyright © 2015 Jawbone. All rights reserved. +// + +#import "JBLineChartLinesView.h" + +// Models +#import "JBLineChartPoint.h" + +// Numerics +CGFloat const kJBLineChartLinesViewMiterLimit = -5.0; +CGFloat const kJBLineChartLinesViewDefaultLinePhase = 1.0f; +CGFloat const kJBLineChartLinesViewSmoothThresholdSlope = 0.01f; +CGFloat const kJBLineChartLinesViewReloadDataAnimationDuration = 0.15f; +NSInteger const kJBLineChartLinesViewSmoothThresholdVertical = 1; +NSInteger const kJBLineChartLinesViewUnselectedLineIndex = -1; + +// Structures +static NSArray *kJBLineChartLinesViewDefaultDashPattern = nil; + +@implementation JBLineChartLinesView + +#pragma mark - Alloc/Init + ++ (void)initialize +{ + if (self == [JBLineChartLinesView class]) + { + kJBLineChartLinesViewDefaultDashPattern = @[@(3), @(2)]; + } +} + +- (id)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self) + { + self.backgroundColor = [UIColor clearColor]; + } + return self; +} + +#pragma mark - Memory Management + +- (void)dealloc +{ + [NSObject cancelPreviousPerformRequestsWithTarget:self]; +} + +#pragma mark - Drawing + +- (void)drawRect:(CGRect)rect +{ + [super drawRect:rect]; + + NSAssert([self.delegate respondsToSelector:@selector(lineChartLinesForLineChartLinesView:)], @"JBLineChartLinesView // delegate must implement - (NSArray *)lineChartLinesForLineChartLinesView:(JBLineChartLinesView *)lineChartLinesView"); + NSArray *chartData = [self.delegate lineChartLinesForLineChartLinesView:self]; + + for (int lineIndex=0; lineIndex<[chartData count]; lineIndex++) + { + JBLineChartLine *lineChartLine = [chartData objectAtIndex:lineIndex]; + { + UIBezierPath *linePath = [self bezierPathForLineChartLine:lineChartLine filled:NO]; + UIBezierPath *fillPath = [self bezierPathForLineChartLine:lineChartLine filled:YES]; + + if (linePath == nil || fillPath == nil) + { + continue; + } + + JBShapeLayer *shapeLayer = [self shapeLayerForLineIndex:lineIndex filled:NO]; + if (shapeLayer == nil) + { + shapeLayer = [[JBShapeLayer alloc] initWithTag:lineIndex filled:NO currentPath:linePath]; + } + + JBShapeLayer *fillLayer = [self shapeLayerForLineIndex:lineIndex filled:YES]; + if (fillLayer == nil) + { + fillLayer = [[JBShapeLayer alloc] initWithTag:lineIndex filled:YES currentPath:nil]; // don't need currentPath since fill's aren't animatable (yet) + } + + shapeLayer.zPosition = 0.1f; + shapeLayer.fillColor = [UIColor clearColor].CGColor; + fillLayer.zPosition = 0.1f; + fillLayer.fillColor = [UIColor clearColor].CGColor; + + // Line style + if (lineChartLine.lineStyle == JBLineChartViewLineStyleSolid) + { + shapeLayer.lineDashPhase = 0.0; + shapeLayer.lineDashPattern = nil; + } + else if (lineChartLine.lineStyle == JBLineChartViewLineStyleDashed) + { + shapeLayer.lineDashPhase = kJBLineChartLinesViewDefaultLinePhase; + shapeLayer.lineDashPattern = kJBLineChartLinesViewDefaultDashPattern; + } + + // Smoothing + if (lineChartLine.smoothedLine) + { + if (lineChartLine.lineStyle == JBLineChartViewLineStyleDashed) + { + shapeLayer.lineCap = kCALineCapButt; // smoothed, dashed lines need butt caps + } + else + { + shapeLayer.lineCap = kCALineCapRound; + } + shapeLayer.lineJoin = kCALineJoinRound; + fillLayer.lineCap = kCALineCapRound; + fillLayer.lineJoin = kCALineJoinRound; + } + else + { + shapeLayer.lineCap = kCALineCapButt; + shapeLayer.lineJoin = kCALineJoinMiter; + fillLayer.lineCap = kCALineCapButt; + fillLayer.lineJoin = kCALineJoinMiter; + } + + // Width + NSAssert([self.delegate respondsToSelector:@selector(lineChartLinesView:widthForLineAtLineIndex:)], @"JBLineChartLinesView // delegate must implement - (CGFloat)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView widthForLineAtLineIndex:(NSUInteger)lineIndex"); + shapeLayer.lineWidth = [self.delegate lineChartLinesView:self widthForLineAtLineIndex:lineIndex]; + fillLayer.lineWidth = [self.delegate lineChartLinesView:self widthForLineAtLineIndex:lineIndex]; + + // Colors + NSAssert([self.delegate respondsToSelector:@selector(lineChartLinesView:colorForLineAtLineIndex:)], @"JBLineChartLinesView // delegate must implement - (UIColor *)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView colorForLineAtLineIndex:(NSUInteger)lineIndex"); + shapeLayer.strokeColor = [self.delegate lineChartLinesView:self colorForLineAtLineIndex:lineIndex].CGColor; + + // Line path + shapeLayer.frame = self.bounds; + if (self.animated) + { + CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:@"path"]; + pathAnimation.fromValue = (id)shapeLayer.currentPath.CGPath; + pathAnimation.toValue = (id)linePath.CGPath; + pathAnimation.duration = kJBLineChartLinesViewReloadDataAnimationDuration; + pathAnimation.timingFunction = [CAMediaTimingFunction functionWithName:@"easeInEaseOut"]; + pathAnimation.fillMode = kCAFillModeBoth; + pathAnimation.removedOnCompletion = NO; + [shapeLayer addAnimation:pathAnimation forKey:@"shapeLayerPathAnimation"]; + } + else + { + shapeLayer.path = linePath.CGPath; + } + shapeLayer.currentPath = [linePath copy]; + + // Fill path + fillLayer.frame = self.bounds; + fillLayer.path = fillPath.CGPath; + + // Solid fill + if (lineChartLine.fillColorStyle == JBLineChartViewColorStyleSolid) + { + NSAssert([self.delegate respondsToSelector:@selector(lineChartLinesView:fillColorForLineAtLineIndex:)], @"JBLineChartLinesView // delegate must implement - (UIColor *)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView fillColorForLineAtLineIndex:(NSUInteger)lineIndex"); + fillLayer.fillColor = [self.delegate lineChartLinesView:self fillColorForLineAtLineIndex:lineIndex].CGColor; + [self.layer addSublayer:fillLayer]; + } + + // Gradient fill + else if (lineChartLine.fillColorStyle == JBLineChartViewColorStyleGradient) + { + JBGradientLayer *fillGradientLayer = [self gradientLayerForLineIndex:lineIndex filled:YES]; + if (fillGradientLayer == nil) + { + NSAssert([self.delegate respondsToSelector:@selector(lineChartLinesView:fillGradientForLineAtLineIndex:)], @"JBLineChartLinesView // delegate must implement - (CAGradientLayer *)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView fillGradientForLineAtLineIndex:(NSUInteger)lineIndex"); + fillGradientLayer = [[JBGradientLayer alloc] initWithGradientLayer:[self.delegate lineChartLinesView:self fillGradientForLineAtLineIndex:lineIndex] tag:lineIndex filled:YES currentPath:nil]; + } + fillGradientLayer.frame = fillLayer.frame; + fillGradientLayer.mask = fillLayer; + [self.layer addSublayer:fillGradientLayer]; + } + + // Solid line + if (lineChartLine.colorStyle == JBLineChartViewColorStyleSolid) + { + [self.layer addSublayer:shapeLayer]; + } + + // Gradient line + else if (lineChartLine.colorStyle == JBLineChartViewColorStyleGradient) + { + JBGradientLayer *gradientLayer = [self gradientLayerForLineIndex:lineIndex filled:NO]; + if (gradientLayer == nil) + { + NSAssert([self.delegate respondsToSelector:@selector(lineChartLinesView:gradientForLineAtLineIndex:)], @"JBLineChartLinesView // delegate must implement - (CAGradientLayer *)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView gradientForLineAtLineIndex:(NSUInteger)lineIndex"); + gradientLayer = [[JBGradientLayer alloc] initWithGradientLayer:[self.delegate lineChartLinesView:self gradientForLineAtLineIndex:lineIndex] tag:lineIndex filled:NO currentPath:linePath]; + } + gradientLayer.frame = shapeLayer.frame; + + if (self.animated) + { + CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:@"path"]; + pathAnimation.fromValue = (id)gradientLayer.currentPath.CGPath; + pathAnimation.toValue = (id)linePath.CGPath; + pathAnimation.duration = kJBLineChartLinesViewReloadDataAnimationDuration; + pathAnimation.timingFunction = [CAMediaTimingFunction functionWithName:@"easeInEaseOut"]; + pathAnimation.fillMode = kCAFillModeBoth; + pathAnimation.removedOnCompletion = NO; + [gradientLayer.mask addAnimation:pathAnimation forKey:@"gradientLayerMaskAnimation"]; + } + else + { + gradientLayer.mask = shapeLayer; + } + gradientLayer.currentPath = [linePath copy]; + + [self.layer addSublayer:gradientLayer]; + } + } + } + + self.animated = NO; +} + +#pragma mark - Data + +- (void)reloadDataAnimated:(BOOL)animated callback:(void (^)())callback +{ + NSAssert([self.delegate respondsToSelector:@selector(lineChartLinesForLineChartLinesView:)], @"JBLineChartLinesView // delegate must implement - (NSArray *)lineChartLinesForLineChartLinesView:(JBLineChartLinesView *)lineChartLinesView"); + NSArray *chartData = [self.delegate lineChartLinesForLineChartLinesView:self]; + + NSUInteger lineCount = [chartData count]; + + __weak JBLineChartLinesView* weakSelf = self; + + dispatch_block_t completionBlock = ^{ + weakSelf.animated = NO; + [weakSelf setNeedsDisplay]; // re-draw layers + if (callback) + { + callback(); + } + }; + + // Mark layers for animation or removal + NSMutableArray *mutableRemovedLayers = [NSMutableArray array]; + for (CALayer *layer in [self.layer sublayers]) + { + BOOL removeLayer = NO; + + if ([layer isKindOfClass:[JBShapeLayer class]]) + { + removeLayer = (((JBShapeLayer *)layer).tag >= lineCount); + } + else if ([layer isKindOfClass:[JBGradientLayer class]]) + { + removeLayer = (((JBGradientLayer *)layer).tag >= lineCount); + } + + if (removeLayer) + { + [mutableRemovedLayers addObject:layer]; + } + } + + // Remove legacy layers + NSArray *removedLayers = [NSArray arrayWithArray:mutableRemovedLayers]; + if ([removedLayers count] > 0) + { + for (int index=0; index<[removedLayers count]; index++) + { + CALayer *removedLayer = [removedLayers objectAtIndex:index]; + + if (animated) + { + [CATransaction begin]; + { + CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"opacity"]; + animation.fromValue = [NSNumber numberWithFloat:1.0f]; + animation.toValue = [NSNumber numberWithFloat:0.0f]; + animation.duration = kJBLineChartLinesViewReloadDataAnimationDuration; + animation.timingFunction = [CAMediaTimingFunction functionWithName:@"easeInEaseOut"]; + animation.fillMode = kCAFillModeBoth; + animation.removedOnCompletion = NO; + + [CATransaction setCompletionBlock:^{ + [removedLayer removeFromSuperlayer]; + if (index == [removedLayers count]-1) + { + completionBlock(); + } + }]; + + [removedLayer addAnimation:animation forKey:@"removeShapeLayerAnimation"]; + } + [CATransaction commit]; + } + else + { + [removedLayer removeFromSuperlayer]; + if (index == [removedLayers count]-1) + { + completionBlock(); + } + } + } + } + else + { + completionBlock(); + } +} + +- (void)reloadDataAnimated:(BOOL)animated +{ + [self reloadDataAnimated:animated callback:nil]; +} + +- (void)reloadData +{ + [self reloadDataAnimated:NO]; +} + +#pragma mark - Setters + +- (void)setSelectedLineIndex:(NSInteger)selectedLineIndex animated:(BOOL)animated +{ + _selectedLineIndex = selectedLineIndex; + + __weak JBLineChartLinesView* weakSelf = self; + + dispatch_block_t adjustLines = ^{ + NSMutableArray *layersToReplace = [NSMutableArray array]; + + NSString * const oldLayerKey = @"oldLayer"; + NSString * const newLayerKey = @"newLayer"; + + for (CALayer *layer in [weakSelf.layer sublayers]) + { + /* + * Solid line or fill + */ + if ([layer isKindOfClass:[JBShapeLayer class]]) + { + JBShapeLayer *shapeLayer = (JBShapeLayer * )layer; + + if (shapeLayer.filled) + { + // Selected solid fill + if (shapeLayer.tag == weakSelf.selectedLineIndex) + { + NSAssert([self.delegate respondsToSelector:@selector(lineChartLinesView:selectionFillColorForLineAtLineIndex:)], @"JBLineChartLinesView // delegate must implement - (UIColor *)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView selectionFillColorForLineAtLineIndex:(NSUInteger)lineIndex"); + shapeLayer.fillColor = [self.delegate lineChartLinesView:self selectionFillColorForLineAtLineIndex:shapeLayer.tag].CGColor; + shapeLayer.opacity = 1.0f; + } + // Unselected solid fill + else + { + NSAssert([self.delegate respondsToSelector:@selector(lineChartLinesView:fillColorForLineAtLineIndex:)], @"JBLineChartLinesView // delegate must implement - (UIColor *)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView fillColorForLineAtLineIndex:(NSUInteger)lineIndex"); + shapeLayer.fillColor = [self.delegate lineChartLinesView:self fillColorForLineAtLineIndex:shapeLayer.tag].CGColor; + + NSAssert([self.delegate respondsToSelector:@selector(lineChartLinesView:dimmedSelectionOpacityAtLineIndex:)], @"JBLineChartLinesView // delegate must implement - (CGFloat)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView dimmedSelectionOpacityAtLineIndex:(NSUInteger)lineIndex"); + shapeLayer.opacity = (weakSelf.selectedLineIndex == kJBLineChartLinesViewUnselectedLineIndex) ? 1.0f : [self.delegate lineChartLinesView:self dimmedSelectionOpacityAtLineIndex:shapeLayer.tag]; + } + } + else + { + // Selected solid line + if (shapeLayer.tag == weakSelf.selectedLineIndex) + { + NSAssert([self.delegate respondsToSelector:@selector(lineChartLinesView:selectionColorForLineAtLineIndex:)], @"JBLineChartLinesView // delegate must implement - (UIColor *)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView selectionColorForLineAtLineIndex:(NSUInteger)lineIndex"); + shapeLayer.strokeColor = [self.delegate lineChartLinesView:self selectionColorForLineAtLineIndex:shapeLayer.tag].CGColor; + shapeLayer.opacity = 1.0f; + } + // Unselected solid line + else + { + NSAssert([self.delegate respondsToSelector:@selector(lineChartLinesView:colorForLineAtLineIndex:)], @"JBLineChartLinesView // delegate must implement - (UIColor *)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView colorForLineAtLineIndex:(NSUInteger)lineIndex"); + shapeLayer.strokeColor = [self.delegate lineChartLinesView:self colorForLineAtLineIndex:shapeLayer.tag].CGColor; + + NSAssert([self.delegate respondsToSelector:@selector(lineChartLinesView:dimmedSelectionOpacityAtLineIndex:)], @"JBLineChartLinesView // delegate must implement - (CGFloat)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView dimmedSelectionOpacityAtLineIndex:(NSUInteger)lineIndex"); + shapeLayer.opacity = (weakSelf.selectedLineIndex == kJBLineChartLinesViewUnselectedLineIndex) ? 1.0f : [self.delegate lineChartLinesView:self dimmedSelectionOpacityAtLineIndex:shapeLayer.tag]; + } + } + } + + /* + * Gradient line or fill + */ + else if ([layer isKindOfClass:[CAGradientLayer class]]) + { + CAGradientLayer *gradientLayer = (CAGradientLayer * )layer; + + if ([gradientLayer.mask isKindOfClass:[JBShapeLayer class]]) + { + JBShapeLayer *shapeLayer = (JBShapeLayer * )gradientLayer.mask; + + if (shapeLayer.filled) + { + // Selected gradient fill + if (shapeLayer.tag == weakSelf.selectedLineIndex) + { + NSAssert([self.delegate respondsToSelector:@selector(lineChartLinesView:selectionFillGradientForLineAtLineIndex:)], @"JBLineChartLinesView // delegate must implement - (CAGradientLayer *)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView selectionFillGradientForLineAtLineIndex:(NSUInteger)lineIndex"); + CAGradientLayer *selectedFillGradient = [self.delegate lineChartLinesView:self selectionFillGradientForLineAtLineIndex:shapeLayer.tag]; + selectedFillGradient.frame = layer.frame; + selectedFillGradient.mask = layer.mask; + selectedFillGradient.opacity = 1.0f; + [layersToReplace addObject:@{oldLayerKey: layer, newLayerKey: selectedFillGradient}]; + } + // Unselected gradient fill + else + { + NSAssert([self.delegate respondsToSelector:@selector(lineChartLinesView:fillGradientForLineAtLineIndex:)], @"JBLineChartLinesView // delegate must implement - (CAGradientLayer *)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView fillGradientForLineAtLineIndex:(NSUInteger)lineIndex"); + CAGradientLayer *unselectedFillGradient = [self.delegate lineChartLinesView:self fillGradientForLineAtLineIndex:shapeLayer.tag]; + unselectedFillGradient.frame = layer.frame; + unselectedFillGradient.mask = layer.mask; + NSAssert([self.delegate respondsToSelector:@selector(lineChartLinesView:dimmedSelectionOpacityAtLineIndex:)], @"JBLineChartLinesView // delegate must implement - (CGFloat)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView dimmedSelectionOpacityAtLineIndex:(NSUInteger)lineIndex"); + unselectedFillGradient.opacity = (weakSelf.selectedLineIndex == kJBLineChartLinesViewUnselectedLineIndex) ? 1.0f : [self.delegate lineChartLinesView:self dimmedSelectionOpacityAtLineIndex:shapeLayer.tag]; + [layersToReplace addObject:@{oldLayerKey: layer, newLayerKey: unselectedFillGradient}]; + } + } + else + { + // Selected gradient line + if (shapeLayer.tag == weakSelf.selectedLineIndex) + { + NSAssert([self.delegate respondsToSelector:@selector(lineChartLinesView:selectionGradientForLineAtLineIndex:)], @"JBLineChartLinesView // delegate must implement - (CAGradientLayer *)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView selectionGradientForLineAtLineIndex:(NSUInteger)lineIndex"); + CAGradientLayer *selectedGradient = [self.delegate lineChartLinesView:self selectionGradientForLineAtLineIndex:shapeLayer.tag]; + selectedGradient.frame = layer.frame; + selectedGradient.mask = layer.mask; + selectedGradient.opacity = 1.0f; + [layersToReplace addObject:@{oldLayerKey: layer, newLayerKey: selectedGradient}]; + } + // Unselected gradient line + else + { + NSAssert([self.delegate respondsToSelector:@selector(lineChartLinesView:gradientForLineAtLineIndex:)], @"JBLineChartLinesView // delegate must implement - (CAGradientLayer *)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView gradientForLineAtLineIndex:(NSUInteger)lineIndex"); + CAGradientLayer *unselectedGradient = [self.delegate lineChartLinesView:self gradientForLineAtLineIndex:shapeLayer.tag]; + unselectedGradient.frame = layer.frame; + unselectedGradient.mask = layer.mask; + NSAssert([self.delegate respondsToSelector:@selector(lineChartLinesView:dimmedSelectionOpacityAtLineIndex:)], @"JBLineChartLinesView // delegate must implement - (CGFloat)lineChartLinesView:(JBLineChartLinesView *)lineChartLinesView dimmedSelectionOpacityAtLineIndex:(NSUInteger)lineIndex"); + shapeLayer.opacity = (weakSelf.selectedLineIndex == kJBLineChartLinesViewUnselectedLineIndex) ? 1.0f : [self.delegate lineChartLinesView:self dimmedSelectionOpacityAtLineIndex:shapeLayer.tag]; + [layersToReplace addObject:@{oldLayerKey: layer, newLayerKey: unselectedGradient}]; + } + } + } + } + } + + for (NSDictionary *layerPair in layersToReplace) + { + [weakSelf.layer replaceSublayer:layerPair[oldLayerKey] with:layerPair[newLayerKey]]; + } + }; + + if (animated) + { + [UIView animateWithDuration:kJBChartViewDefaultAnimationDuration animations:^{ + adjustLines(); + }]; + } + else + { + adjustLines(); + } +} + +- (void)setSelectedLineIndex:(NSInteger)selectedLineIndex +{ + [self setSelectedLineIndex:selectedLineIndex animated:NO]; +} + +#pragma mark - Getters + +- (UIBezierPath *)bezierPathForLineChartLine:(JBLineChartLine *)lineChartLine filled:(BOOL)filled +{ + if ([lineChartLine.lineChartPoints count] > 0) + { + UIBezierPath *bezierPath = [UIBezierPath bezierPath]; + + bezierPath.miterLimit = kJBLineChartLinesViewMiterLimit; + + JBLineChartPoint *previousLineChartPoint = nil; + CGFloat previousSlope = 0.0f; + + BOOL visiblePointFound = NO; + NSArray *sortedLineChartPoints = [lineChartLine.lineChartPoints sortedArrayUsingSelector:@selector(compare:)]; + CGFloat firstXPosition = 0.0f; + CGFloat firstYPosition = 0.0f; + CGFloat lastXPosition = 0.0f; + CGFloat lastYPosition = 0.0f; + + for (NSUInteger index=0; index<[sortedLineChartPoints count]; index++) + { + JBLineChartPoint *lineChartPoint = [sortedLineChartPoints objectAtIndex:index]; + + if (lineChartPoint.hidden) + { + continue; + } + + if (!visiblePointFound) + { + [bezierPath moveToPoint:CGPointMake(lineChartPoint.position.x, lineChartPoint.position.y)]; + firstXPosition = lineChartPoint.position.x; + firstYPosition = lineChartPoint.position.y; + visiblePointFound = YES; + } + else + { + JBLineChartPoint *nextLineChartPoint = nil; + if (index != ([lineChartLine.lineChartPoints count] - 1)) + { + nextLineChartPoint = [sortedLineChartPoints objectAtIndex:(index + 1)]; + } + + CGFloat nextSlope = (nextLineChartPoint != nil) ? ((nextLineChartPoint.position.y - lineChartPoint.position.y)) / ((nextLineChartPoint.position.x - lineChartPoint.position.x)) : previousSlope; + CGFloat currentSlope = ((lineChartPoint.position.y - previousLineChartPoint.position.y)) / (lineChartPoint.position.x-previousLineChartPoint.position.x); + + BOOL deltaFromNextSlope = ((currentSlope >= (nextSlope + kJBLineChartLinesViewSmoothThresholdSlope)) || (currentSlope <= (nextSlope - kJBLineChartLinesViewSmoothThresholdSlope))); + BOOL deltaFromPreviousSlope = ((currentSlope >= (previousSlope + kJBLineChartLinesViewSmoothThresholdSlope)) || (currentSlope <= (previousSlope - kJBLineChartLinesViewSmoothThresholdSlope))); + BOOL deltaFromPreviousY = (lineChartPoint.position.y >= previousLineChartPoint.position.y + kJBLineChartLinesViewSmoothThresholdVertical) || (lineChartPoint.position.y <= previousLineChartPoint.position.y - kJBLineChartLinesViewSmoothThresholdVertical); + + if (lineChartLine.smoothedLine && deltaFromNextSlope && deltaFromPreviousSlope && deltaFromPreviousY) + { + CGFloat deltaX = lineChartPoint.position.x - previousLineChartPoint.position.x; + CGFloat controlPointX = previousLineChartPoint.position.x + (deltaX / 2); + + CGPoint controlPoint1 = CGPointMake(controlPointX, previousLineChartPoint.position.y); + CGPoint controlPoint2 = CGPointMake(controlPointX, lineChartPoint.position.y); + + [bezierPath addCurveToPoint:CGPointMake(lineChartPoint.position.x, lineChartPoint.position.y) controlPoint1:controlPoint1 controlPoint2:controlPoint2]; + } + else + { + [bezierPath addLineToPoint:CGPointMake(lineChartPoint.position.x, lineChartPoint.position.y)]; + } + + lastXPosition = lineChartPoint.position.x; + lastYPosition = lineChartPoint.position.y; + previousSlope = currentSlope; + } + previousLineChartPoint = lineChartPoint; + } + + if (filled) + { + UIBezierPath *filledBezierPath = [bezierPath copy]; + + if(visiblePointFound) + { + [filledBezierPath addLineToPoint:CGPointMake(lastXPosition, lastYPosition)]; + [filledBezierPath addLineToPoint:CGPointMake(lastXPosition, self.bounds.size.height)]; + + [filledBezierPath addLineToPoint:CGPointMake(firstXPosition, self.bounds.size.height)]; + [filledBezierPath addLineToPoint:CGPointMake(firstXPosition, firstYPosition)]; + } + + return filledBezierPath; + } + else + { + return bezierPath; + } + } + return nil; +} + +- (JBShapeLayer *)shapeLayerForLineIndex:(NSUInteger)lineIndex filled:(BOOL)filled +{ + for (CALayer *layer in [self.layer sublayers]) + { + if ([layer isKindOfClass:[JBShapeLayer class]]) + { + if (((JBShapeLayer *)layer).tag == lineIndex && ((JBShapeLayer *)layer).filled == filled) + { + return (JBShapeLayer *)layer; + } + } + } + return nil; +} + +- (JBGradientLayer *)gradientLayerForLineIndex:(NSUInteger)lineIndex filled:(BOOL)filled +{ + for (CALayer *layer in [self.layer sublayers]) + { + if ([layer isKindOfClass:[JBGradientLayer class]]) + { + if (((JBGradientLayer *)layer).tag == lineIndex && ((JBGradientLayer *)layer).filled == filled) + { + return (JBGradientLayer *)layer; + } + } + } + return nil; +} + +#pragma mark - Callback Helpers + +- (void)fireCallback:(void (^)())callback +{ + dispatch_block_t callbackCopy = [callback copy]; + + if (callbackCopy != nil) + { + callbackCopy(); + } +} + +@end