More refactors

This commit is contained in:
Terry Worona
2015-12-26 00:48:01 -05:00
parent d1b7c13e8c
commit e43db2cbb1
6 changed files with 712 additions and 646 deletions
+13
View File
@@ -0,0 +1,13 @@
//
// JBLineChartLinesView.h
// JBChartViewDemo
//
// Created by Terry Worona on 12/26/15.
// Copyright © 2015 Jawbone. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface JBLineChartLinesView : NSObject
@end
+13
View File
@@ -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
@@ -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 = "<group>"; };
5683C5781C2E54100017B6BA /* JBLineChartDotView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JBLineChartDotView.h; sourceTree = "<group>"; };
5683C5791C2E54100017B6BA /* JBLineChartDotView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JBLineChartDotView.m; sourceTree = "<group>"; };
5683C57E1C2E597D0017B6BA /* JBLineChartLinesView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JBLineChartLinesView.h; sourceTree = "<group>"; };
5683C57F1C2E597D0017B6BA /* JBLineChartLinesView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JBLineChartLinesView.m; sourceTree = "<group>"; };
94BDFC3219F933B2007492F6 /* JBLineChartMissingPointsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JBLineChartMissingPointsViewController.h; sourceTree = "<group>"; };
94BDFC3319F933B2007492F6 /* JBLineChartMissingPointsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JBLineChartMissingPointsViewController.m; sourceTree = "<group>"; };
9B07251F1829822A0052109B /* JBChartListViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JBChartListViewController.h; sourceTree = "<group>"; };
@@ -231,6 +234,8 @@
5683C5731C2E512C0017B6BA /* JBLineChartDotsView.m */,
5683C5781C2E54100017B6BA /* JBLineChartDotView.h */,
5683C5791C2E54100017B6BA /* JBLineChartDotView.m */,
5683C57E1C2E597D0017B6BA /* JBLineChartLinesView.h */,
5683C57F1C2E597D0017B6BA /* JBLineChartLinesView.m */,
);
path = Views;
sourceTree = "<group>";
@@ -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 */,
@@ -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<JBLineChartLinesViewDelegate> 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 <NSObject>
- (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 () <JBLineChartLinesViewDelegate, JBLineChartDotsViewDelegate>
@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
@@ -0,0 +1,61 @@
//
// JBLineChartLinesView.h
// JBChartViewDemo
//
// Created by Terry Worona on 12/26/15.
// Copyright © 2015 Jawbone. All rights reserved.
//
#import <Foundation/Foundation.h>
// 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<JBLineChartLinesViewDelegate> 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 <NSObject>
- (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
@@ -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