From 0322a298ad7a2fbb6a70761f1dc12a7834ad9a11 Mon Sep 17 00:00:00 2001 From: mr-brandt Date: Fri, 4 Jul 2014 21:56:34 +0200 Subject: [PATCH 1/2] - Added touch support to GKLineGraph (see demo for an example) - Added delegate to detect touches in GKLineGraph - Added support to configure the line-dashpattern of the gridlines in GKLineGraph - Added support to display x- and y-axis in GKLineGraph - Added support to display gridlines in GKLineGraph - Added support to configure the color of the axes and labels in GKLineGraph - Added support to configure the color of the gridlines in GKLineGraph --- GraphKit/Example/ExampleLineGraph.m | 60 +++- GraphKit/GraphKit.xcodeproj/project.pbxproj | 6 + Source/LineGraph/GKLineDataPoint.h | 19 ++ Source/LineGraph/GKLineDataPoint.m | 36 +++ Source/LineGraph/GKLineGraph.h | 34 ++- Source/LineGraph/GKLineGraph.m | 316 +++++++++++++++++++- 6 files changed, 450 insertions(+), 21 deletions(-) mode change 100644 => 100755 GraphKit/Example/ExampleLineGraph.m create mode 100644 Source/LineGraph/GKLineDataPoint.h create mode 100644 Source/LineGraph/GKLineDataPoint.m mode change 100644 => 100755 Source/LineGraph/GKLineGraph.h mode change 100644 => 100755 Source/LineGraph/GKLineGraph.m diff --git a/GraphKit/Example/ExampleLineGraph.m b/GraphKit/Example/ExampleLineGraph.m old mode 100644 new mode 100755 index 2f1cc97..0378f15 --- a/GraphKit/Example/ExampleLineGraph.m +++ b/GraphKit/Example/ExampleLineGraph.m @@ -10,7 +10,7 @@ #import "UIViewController+BButton.h" -@interface ExampleLineGraph () +@interface ExampleLineGraph () @end @@ -32,17 +32,26 @@ - (void)viewDidLoad { - (void)_setupExampleGraph { self.data = @[ - @[@20, @40, @20, @60, @40, @140, @80], + @[@-20, @-80, @20, @60, @40, @140, @80], @[@40, @20, @60, @100, @60, @20, @60], @[@80, @60, @40, @160, @100, @40, @110], @[@120, @150, @80, @120, @140, @100, @0], -// @[@620, @650, @580, @620, @540, @400, @0] + // @[@620, @650, @580, @620, @540, @400, @0] ]; self.labels = @[@"2001", @"2002", @"2003", @"2004", @"2005", @"2006", @"2007"]; self.graph.dataSource = self; - self.graph.lineWidth = 3.0; + self.graph.delegate = self; + self.graph.lineWidth = 1.5; + self.graph.ensureXAxisVisibility = YES; + self.graph.drawHorizontalGridLines = YES; + self.graph.drawVerticalGridLines = NO; + self.graph.drawCoordinateSystem = YES; + + self.graph.coordinateSystemColor = [UIColor darkGrayColor]; + self.graph.gridLinesColor = [UIColor lightGrayColor]; + self.graph.labelTextColor = [UIColor darkGrayColor]; self.graph.valueLabelCount = 6; @@ -60,10 +69,6 @@ - (void)_setupTestingGraphLow { @[@10, @4, @8, @2, @9, @3, @6], @[@1, @2, @3, @4, @5, @6, @10] ]; -// self.data = @[ -// @[@2, @2, @2, @2, @2, @2, @6], -// @[@1, @1, @1, @1, @1, @1, @1] -// ]; self.labels = @[@"2001", @"2002", @"2003", @"2004", @"2005", @"2006", @"2007"]; @@ -73,6 +78,8 @@ - (void)_setupTestingGraphLow { // self.graph.startFromZero = YES; self.graph.valueLabelCount = 10; + self.graph.ensureXAxisVisibility = YES; + [self.graph draw]; } @@ -120,7 +127,8 @@ - (UIColor *)colorForLineAtIndex:(NSInteger)index { id colors = @[[UIColor gk_turquoiseColor], [UIColor gk_peterRiverColor], [UIColor gk_alizarinColor], - [UIColor gk_sunflowerColor] + [UIColor gk_sunflowerColor], + [UIColor gk_amethystColor] ]; return [colors objectAtIndex:index]; } @@ -130,11 +138,43 @@ - (NSArray *)valuesForLineAtIndex:(NSInteger)index { } - (CFTimeInterval)animationDurationForLineAtIndex:(NSInteger)index { - return [[@[@1, @1.6, @2.2, @1.4] objectAtIndex:index] doubleValue]; + // return [[@[@1, @1.6, @2.2, @1.4] objectAtIndex:index] doubleValue]; + return 0.75f; } - (NSString *)titleForLineAtIndex:(NSInteger)index { return [self.labels objectAtIndex:index]; } +- (NSArray* ) dashPatternForLineAtIndex:(NSInteger)index +{ + return [NSArray arrayWithObjects:[NSDecimalNumber numberWithInt:3],[NSDecimalNumber numberWithInt:3],nil]; +} + +#pragma mark - GKLineGraphDelegate +- (void)lineGraph:(GKLineGraph *)lineGraph willSelectDataPoint:(GKLineDataPoint *)dataPoint AtPoint:(CGPoint)targetPoint +{ + +} + +- (void)lineGraph:(GKLineGraph *)lineGraph didSelectDataPoint:(GKLineDataPoint *)dataPoint AtPoint:(CGPoint)targetPoint +{ + NSLog (@"Did select value %i on line #%i", dataPoint.valueIndex, dataPoint.lineIndex); +} + +- (void)lineGraph:(GKLineGraph *)lineGraph willDeselectDataPoint:(GKLineDataPoint *)dataPoint AtPoint:(CGPoint)targetPoint +{ + +} + +- (void)lineGraph:(GKLineGraph *)lineGraph didDeselectDataPoint:(GKLineDataPoint *)dataPoint AtPoint:(CGPoint)targetPoint +{ + NSLog (@"Did deselect value %i on line #%i", dataPoint.valueIndex, dataPoint.lineIndex); +} + +- (void)lineGraph:(GKLineGraph *)lineGraph didReselectDataPoint:(GKLineDataPoint *)dataPoint AtPoint:(CGPoint)targetPoint +{ + +} + @end diff --git a/GraphKit/GraphKit.xcodeproj/project.pbxproj b/GraphKit/GraphKit.xcodeproj/project.pbxproj index 0ae11f9..9849615 100644 --- a/GraphKit/GraphKit.xcodeproj/project.pbxproj +++ b/GraphKit/GraphKit.xcodeproj/project.pbxproj @@ -33,6 +33,7 @@ 84F0E98E1902B30C00FA4810 /* ExampleListVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 84F0E98C1902B30C00FA4810 /* ExampleListVC.m */; }; 84F0E98F1902B30C00FA4810 /* ExampleListVC.xib in Resources */ = {isa = PBXBuildFile; fileRef = 84F0E98D1902B30C00FA4810 /* ExampleListVC.xib */; }; 973F4746059343C9A462AA2D /* libPods.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 74E12B5B49644056930176BC /* libPods.a */; }; + B758FCEB19673B7C00F843E5 /* GKLineDataPoint.m in Sources */ = {isa = PBXBuildFile; fileRef = B758FCEA19673B7C00F843E5 /* GKLineDataPoint.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -87,6 +88,8 @@ 84F0E98B1902B30C00FA4810 /* ExampleListVC.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ExampleListVC.h; sourceTree = ""; }; 84F0E98C1902B30C00FA4810 /* ExampleListVC.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ExampleListVC.m; sourceTree = ""; }; 84F0E98D1902B30C00FA4810 /* ExampleListVC.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ExampleListVC.xib; sourceTree = ""; }; + B758FCE919673B7C00F843E5 /* GKLineDataPoint.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GKLineDataPoint.h; sourceTree = ""; }; + B758FCEA19673B7C00F843E5 /* GKLineDataPoint.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GKLineDataPoint.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -236,6 +239,8 @@ 847259201904AB8F00819664 /* LineGraph */ = { isa = PBXGroup; children = ( + B758FCE919673B7C00F843E5 /* GKLineDataPoint.h */, + B758FCEA19673B7C00F843E5 /* GKLineDataPoint.m */, 847259211904ABE200819664 /* GKLineGraph.h */, 847259221904ABE200819664 /* GKLineGraph.m */, ); @@ -382,6 +387,7 @@ 84654C2418FE8EE300160F11 /* AppDelegate.m in Sources */, 847259291905C1D300819664 /* UIColor+GraphKit.m in Sources */, 84654C5918FF4C8300160F11 /* ExampleBarGraphVC.m in Sources */, + B758FCEB19673B7C00F843E5 /* GKLineDataPoint.m in Sources */, 8472591E1904AA4F00819664 /* ExampleLineGraph.m in Sources */, 84654C4C18FE909A00160F11 /* GKBar.m in Sources */, 84654C2018FE8EE300160F11 /* main.m in Sources */, diff --git a/Source/LineGraph/GKLineDataPoint.h b/Source/LineGraph/GKLineDataPoint.h new file mode 100644 index 0000000..07bace2 --- /dev/null +++ b/Source/LineGraph/GKLineDataPoint.h @@ -0,0 +1,19 @@ +// +// GKLineDataPoint.h +// GraphKit +// +// Created by Martin Brandt on 03.07.14. +// Copyright (c) 2014 Michal Konturek. All rights reserved. +// + +#import + +@interface GKLineDataPoint : NSObject + +@property (nonatomic, assign) NSInteger lineIndex; +@property (nonatomic, assign) NSInteger valueIndex; + +- (BOOL) isEmptyDataPoint; +- (BOOL) isEqual:(GKLineDataPoint *)dataPoint; + +@end diff --git a/Source/LineGraph/GKLineDataPoint.m b/Source/LineGraph/GKLineDataPoint.m new file mode 100644 index 0000000..786667c --- /dev/null +++ b/Source/LineGraph/GKLineDataPoint.m @@ -0,0 +1,36 @@ +// +// GKLineDataPoint.m +// GraphKit +// +// Created by Martin Brandt on 03.07.14. +// Copyright (c) 2014 Michal Konturek. All rights reserved. +// + +#import "GKLineDataPoint.h" + +@implementation GKLineDataPoint + +- (instancetype) init { + self = [super init]; + if (self) { + [self _init]; + } + return self; +} + +- (void)_init { + self.lineIndex = -1; + self.valueIndex = -1; +} + +- (BOOL) isEmptyDataPoint +{ + return _lineIndex == -1 && _valueIndex == -1; +} + +- (BOOL) isEqual:(GKLineDataPoint *)dataPoint +{ + return _lineIndex == dataPoint.lineIndex && _valueIndex == dataPoint.valueIndex; +} + +@end diff --git a/Source/LineGraph/GKLineGraph.h b/Source/LineGraph/GKLineGraph.h old mode 100644 new mode 100755 index 6e592e9..6d719ec --- a/Source/LineGraph/GKLineGraph.h +++ b/Source/LineGraph/GKLineGraph.h @@ -24,24 +24,35 @@ // #import +#import "GKLineDataPoint.h" @protocol GKLineGraphDataSource; +@protocol GKLineGraphDelegate; @interface GKLineGraph : UIView @property (nonatomic, assign) BOOL animated; @property (nonatomic, assign) CFTimeInterval animationDuration; +@property (nonatomic, weak, readwrite) id delegate; @property (nonatomic, assign) id dataSource; @property (nonatomic, assign) CGFloat lineWidth; @property (nonatomic, assign) CGFloat margin; @property (nonatomic, assign) NSInteger valueLabelCount; -//@property (nonatomic, strong) NSNumber *maxValue; @property (nonatomic, assign) CGFloat *minValue; @property (nonatomic, assign) BOOL startFromZero; +@property (nonatomic, assign) BOOL ensureXAxisVisibility; +@property (nonatomic, assign) BOOL drawCoordinateSystem; +@property (nonatomic, assign) BOOL drawVerticalGridLines; +@property (nonatomic, assign) BOOL drawHorizontalGridLines; +@property (nonatomic, assign) NSInteger touchDistanceThreshold; + +@property (nonatomic, strong) UIColor *coordinateSystemColor; +@property (nonatomic, strong) UIColor *gridLinesColor; +@property (nonatomic, strong) UIColor *labelTextColor; - (void)draw; - (void)reset; @@ -50,13 +61,24 @@ @protocol GKLineGraphDataSource -- (NSInteger)numberOfLines; -- (UIColor *)colorForLineAtIndex:(NSInteger)index; -- (NSArray *)valuesForLineAtIndex:(NSInteger)index; +- (NSInteger) numberOfLines; +- (UIColor *) colorForLineAtIndex: (NSInteger)index; +- (NSArray *) valuesForLineAtIndex: (NSInteger)index; @optional -- (CFTimeInterval)animationDurationForLineAtIndex:(NSInteger)index; -- (NSString *)titleForLineAtIndex:(NSInteger)index; +- (CFTimeInterval) animationDurationForLineAtIndex:(NSInteger)index; +- (NSString *) titleForLineAtIndex:(NSInteger)index; +- (NSArray *) dashPatternForLineAtIndex: (NSInteger)index; + +@end + +@protocol GKLineGraphDelegate + +- (void)lineGraph:(GKLineGraph *)lineGraph willSelectDataPoint:(GKLineDataPoint *)dataPoint AtPoint:(CGPoint) targetPoint; +- (void)lineGraph:(GKLineGraph *)lineGraph didSelectDataPoint:(GKLineDataPoint *)dataPoint AtPoint:(CGPoint) targetPoint; +- (void)lineGraph:(GKLineGraph *)lineGraph willDeselectDataPoint:(GKLineDataPoint *)dataPoint AtPoint:(CGPoint) targetPoint; +- (void)lineGraph:(GKLineGraph *)lineGraph didDeselectDataPoint:(GKLineDataPoint *)dataPoint AtPoint:(CGPoint) targetPoint; +- (void)lineGraph:(GKLineGraph *)lineGraph didReselectDataPoint:(GKLineDataPoint *)dataPoint AtPoint:(CGPoint) targetPoint; @end diff --git a/Source/LineGraph/GKLineGraph.m b/Source/LineGraph/GKLineGraph.m old mode 100644 new mode 100755 index 9eb9849..985e57a --- a/Source/LineGraph/GKLineGraph.m +++ b/Source/LineGraph/GKLineGraph.m @@ -32,6 +32,9 @@ static CGFloat kDefaultLabelHeight = 12.0; static NSInteger kDefaultValueLabelCount = 5; +static NSInteger kCoordinateLayerIndexTag = -1; +static CGFloat kDefaultTouchDistanceThreshold = 5.0; + static CGFloat kDefaultLineWidth = 3.0; static CGFloat kDefaultMargin = 10.0; static CGFloat kDefaultMarginBottom = 20.0; @@ -43,6 +46,8 @@ @interface GKLineGraph () @property (nonatomic, strong) NSArray *titleLabels; @property (nonatomic, strong) NSArray *valueLabels; +@property (nonatomic, strong) GKLineDataPoint *selectedDataPoint; + @end @implementation GKLineGraph @@ -70,6 +75,11 @@ - (void)_init { self.margin = kDefaultMargin; self.valueLabelCount = kDefaultValueLabelCount; self.clipsToBounds = YES; + self.selectedDataPoint = [GKLineDataPoint new]; + self.touchDistanceThreshold = kDefaultTouchDistanceThreshold; + self.coordinateSystemColor = [UIColor darkGrayColor]; + self.gridLinesColor = [UIColor lightGrayColor]; + self.labelTextColor = [UIColor darkGrayColor]; } - (void)draw { @@ -85,6 +95,11 @@ - (void)draw { [self _drawLines]; } +- (void) setTouchDistanceThreshold:(NSInteger)touchDistanceThreshold +{ + _touchDistanceThreshold = touchDistanceThreshold * [[UIScreen mainScreen] scale]; +} + - (BOOL)_hasTitleLabels { return ![self.titleLabels mk_isEmpty]; } @@ -103,7 +118,7 @@ - (void)_constructTitleLabels { UILabel *item = [[UILabel alloc] initWithFrame:frame]; item.textAlignment = NSTextAlignmentCenter; item.font = [UIFont boldSystemFontOfSize:12]; - item.textColor = [UIColor lightGrayColor]; + item.textColor = _labelTextColor; item.text = [self.dataSource titleForLineAtIndex:idx]; [items addObject:item]; @@ -139,10 +154,15 @@ - (void)_positionTitleLabels { }]; } -- (CGFloat)_pointXForIndex:(NSInteger)index { +- (CGFloat)_pointXForIndex:(float)index { return kAxisMargin + self.margin + (index * [self _stepX]); } +- (CGFloat)_indexForPointX:(float)pointX { + CGFloat index = (pointX - kAxisMargin - self.margin) / [self _stepX]; + return roundf(index); +} + - (CGFloat)_stepX { id values = [self.dataSource valuesForLineAtIndex:0]; CGFloat result = ([self _plotWidth] / [values count]); @@ -180,13 +200,17 @@ - (CGFloat)_stepValueLabelY { - (CGFloat)_maxValue { id values = [self _allValues]; - return [[values mk_max] floatValue]; + float realMaxValue = [[values mk_max] floatValue]; + + return (_ensureXAxisVisibility ? MAX (0, realMaxValue) : realMaxValue); } - (CGFloat)_minValue { if (self.startFromZero) return 0; id values = [self _allValues]; - return [[values mk_min] floatValue]; + + float realMinValue = [[values mk_min] floatValue]; + return (_ensureXAxisVisibility ? MIN (0, realMinValue) : realMinValue); } - (NSArray *)_allValues { @@ -215,9 +239,161 @@ - (CGFloat)_plotHeight { } - (void)_drawLines { + NSInteger numValues = 0; for (NSInteger idx = 0; idx < [self.dataSource numberOfLines]; idx++) { + numValues = MAX(numValues, [[self.dataSource valuesForLineAtIndex:idx] count]); + [self _drawLineAtIndex:idx]; } + + if (self.drawVerticalGridLines) + { + [self _drawVerticalGridLines:numValues]; + } + if (self.drawHorizontalGridLines) + { + [self _drawHorizontalGridLines:numValues]; + } + if (self.drawCoordinateSystem) + { + [self _drawCoordinateSystem:numValues]; + } +} + +- (void)_drawHorizontalGridLines:(NSInteger) indexCount { + UIGraphicsBeginImageContext(self.frame.size); + + UIBezierPath *path = [self _bezierPathWith:0]; + CAShapeLayer *layer = [self _layerWithPath:path]; + [layer setValue:[NSNumber numberWithInteger:kCoordinateLayerIndexTag] forKey:@"indexTag"]; + layer.strokeColor = [_gridLinesColor CGColor]; + layer.lineWidth = MAX (1,self.lineWidth/2); + NSArray *dashPattern = [NSArray arrayWithObjects:[NSDecimalNumber numberWithInt:2],[NSDecimalNumber numberWithInt:2],nil]; + [layer setLineDashPattern:dashPattern]; + + [self.layer addSublayer:layer]; + + NSInteger count = self.valueLabelCount; + for (NSInteger idx = 0; idx < count; idx++) + { + UILabel *label = [self.valueLabels objectAtIndex:idx]; + CGFloat yCoordinate = label.frame.origin.y + (label.frame.size.height / 2); + // NSLog(@"yValue: %f", yValue); + + [path moveToPoint:CGPointMake([self _pointXForIndex:0], yCoordinate)]; + [path addLineToPoint:CGPointMake([self _pointXForIndex:0], yCoordinate)]; + [path addLineToPoint:CGPointMake([self _pointXForIndex:indexCount], yCoordinate)]; + } + + layer.path = path.CGPath; + + if (self.animated) { + CABasicAnimation *animation = [self _animationWithKeyPath:@"strokeEnd"]; + if ([self.dataSource respondsToSelector:@selector(animationDurationForLineAtIndex:)]) { + animation.duration = [self.dataSource animationDurationForLineAtIndex:0]; + } + [layer addAnimation:animation forKey:@"strokeEndAnimation"]; + } + + UIGraphicsEndImageContext(); +} + +- (void)_drawVerticalGridLines:(NSInteger) indexCount { + UIGraphicsBeginImageContext(self.frame.size); + + UIBezierPath *path = [self _bezierPathWith:0]; + CAShapeLayer *layer = [self _layerWithPath:path]; + [layer setValue:[NSNumber numberWithInteger:kCoordinateLayerIndexTag] forKey:@"indexTag"]; + layer.strokeColor = [_gridLinesColor CGColor]; + layer.lineWidth = MAX (1,self.lineWidth/2); + NSArray *dashPattern = [NSArray arrayWithObjects:[NSDecimalNumber numberWithInt:2],[NSDecimalNumber numberWithInt:2],nil]; + [layer setLineDashPattern:dashPattern]; + + [self.layer addSublayer:layer]; + + for (int step = 1; step < indexCount; step++) + { + [path moveToPoint:CGPointMake([self _pointXForIndex:step], [self _positionYForLineValue:[self _maxValue]])]; + [path addLineToPoint:CGPointMake([self _pointXForIndex:step], [self _positionYForLineValue:[self _maxValue]])]; + [path addLineToPoint:CGPointMake([self _pointXForIndex:step], [self _positionYForLineValue:[self _minValue]])]; + // bezierPath.lineJoinStyle = kCGLineJoinRound; + } + + layer.path = path.CGPath; + + if (self.animated) { + CABasicAnimation *animation = [self _animationWithKeyPath:@"strokeEnd"]; + if ([self.dataSource respondsToSelector:@selector(animationDurationForLineAtIndex:)]) { + animation.duration = [self.dataSource animationDurationForLineAtIndex:0]; + } + [layer addAnimation:animation forKey:@"strokeEndAnimation"]; + } + + UIGraphicsEndImageContext(); +} + +- (void)_drawCoordinateSystem:(NSInteger) indexCount { + // http://stackoverflow.com/questions/19599266/invalid-context-0x0-under-ios-7-0-and-system-degradation + UIGraphicsBeginImageContext(self.frame.size); + + UIBezierPath *path = [self _bezierPathWith:0]; + CAShapeLayer *layer = [self _layerWithPath:path]; + [layer setValue:[NSNumber numberWithInteger:kCoordinateLayerIndexTag] forKey:@"indexTag"]; + + layer.strokeColor = [_coordinateSystemColor CGColor]; + + [self.layer addSublayer:layer]; + + // Draw y Coordinate Line + CGFloat x = [self _pointXForIndex:0]; + CGFloat y = [self _positionYForLineValue:[self _minValue]]; + CGPoint point = CGPointMake(x, y); + + [path moveToPoint:point]; + [path addLineToPoint:point]; + + x = [self _pointXForIndex:0]; + y = [self _positionYForLineValue:[self _maxValue]]; + point = CGPointMake(x, y); + + [path addLineToPoint:point]; + + // Draw x Coordinate Line + x = [self _pointXForIndex:0]; + y = [self _positionYForLineValue:0.0]; + point = CGPointMake(x, y); + + [path moveToPoint:point]; + [path addLineToPoint:point]; + + x = [self _pointXForIndex:indexCount]; + point = CGPointMake(x, y); + + [path addLineToPoint:point]; + + // Draw x Copordinate Caps + float maxCapValue = 0.03125 * MAX([self _maxValue], fabs([self _minValue])); + float minCapValue = -1 * maxCapValue; + + for (int step = 1; step < indexCount; step++) + { + [path moveToPoint:CGPointMake([self _pointXForIndex:step], [self _positionYForLineValue:maxCapValue])]; + [path addLineToPoint:CGPointMake([self _pointXForIndex:step], [self _positionYForLineValue:maxCapValue])]; + [path addLineToPoint:CGPointMake([self _pointXForIndex:step], [self _positionYForLineValue:minCapValue])]; + // bezierPath.lineJoinStyle = kCGLineJoinRound; + } + + layer.path = path.CGPath; + + if (self.animated) { + CABasicAnimation *animation = [self _animationWithKeyPath:@"strokeEnd"]; + if ([self.dataSource respondsToSelector:@selector(animationDurationForLineAtIndex:)]) { + animation.duration = [self.dataSource animationDurationForLineAtIndex:0]; + } + [layer addAnimation:animation forKey:@"strokeEndAnimation"]; + } + + UIGraphicsEndImageContext(); } - (void)_drawLineAtIndex:(NSInteger)index { @@ -228,6 +404,14 @@ - (void)_drawLineAtIndex:(NSInteger)index { UIBezierPath *path = [self _bezierPathWith:0]; CAShapeLayer *layer = [self _layerWithPath:path]; + if ([_dataSource respondsToSelector:@selector(dashPatternForLineAtIndex:)]) + { + NSArray *dashPattern = [self.dataSource dashPatternForLineAtIndex:index]; + [layer setLineDashPattern:dashPattern]; + } + + [layer setValue:[NSNumber numberWithInteger:index] forKey:@"indexTag"]; + layer.strokeColor = [[self.dataSource colorForLineAtIndex:index] CGColor]; [self.layer addSublayer:layer]; @@ -240,6 +424,8 @@ - (void)_drawLineAtIndex:(NSInteger)index { CGFloat y = [self _positionYForLineValue:[item floatValue]]; CGPoint point = CGPointMake(x, y); + // NSLog (@"Value %f at Point for Layer %i at Index %i: - %f/%f", [item floatValue], index, idx, point.x, point.y); + if (idx != 0) [path addLineToPoint:point]; [path moveToPoint:point]; @@ -262,7 +448,7 @@ - (void)_drawLineAtIndex:(NSInteger)index { - (CGFloat)_positionYForLineValue:(CGFloat)value { CGFloat scale = (value - [self _minValue]) / ([self _maxValue] - [self _minValue]); CGFloat result = [self _plotHeight] * scale; - result = ([self _plotHeight] - result); + result = ([self _plotHeight] - result); result += kDefaultLabelHeight; return result; } @@ -301,6 +487,126 @@ - (void)reset { self.layer.sublayers = nil; [self _removeTitleLabels]; [self _removeValueLabels]; + self.selectedDataPoint = [GKLineDataPoint new]; +} + +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event +{ + [self touchesMoved:touches withEvent:event]; +} + +void processPathElement(void* info, const CGPathElement* element) +{ + CGPoint pointArg = element->points[0]; + CGFloat distance = 0; + + if (element->type == kCGPathElementMoveToPoint) + { + NSMutableDictionary *theDict = (__bridge NSMutableDictionary *) info; + + NSValue *pointValue = theDict[@"thePoint"]; + CGPoint touchedPoint = [pointValue CGPointValue]; + + CGFloat xDist = (touchedPoint.x - pointArg.x); + CGFloat yDist = (touchedPoint.y - pointArg.y); + distance = sqrt((xDist * xDist) + (yDist * yDist)); + + NSNumber *thresholdNumber = (NSNumber*)theDict[@"touchThreshold"]; + NSInteger touchDistanceThreshold = [thresholdNumber integerValue]; + + if (distance <= touchDistanceThreshold) + { + theDict[@"pointOnPath"] = [NSNumber numberWithBool:YES]; + } + + // NSLog(@"Type: %u || Point: %@ || Distance: %f", element->type, NSStringFromCGPoint(pointArg), distance); + } +} + +- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event +{ + UITouch *touch = [touches anyObject]; + CGPoint point = [touch locationInView:self]; + + NSArray *subLayers = self.layer.sublayers; + + // NSLog (@"Point: %f/%f", point.x, point.y); + + NSMutableDictionary *dict = [@{ @"thePoint" : [NSValue valueWithCGPoint:point] } mutableCopy]; + dict[@"pointOnPath"] = [NSNumber numberWithBool:NO]; + dict[@"touchThreshold"] = [NSNumber numberWithInteger:_touchDistanceThreshold]; + + GKLineDataPoint *selectedDataPoint = [GKLineDataPoint new]; + + [subLayers enumerateObjectsUsingBlock:^(id obj, NSUInteger index, BOOL *stop) + { + // CALayer *layer = (CALayer *) obj; + // NSLog (@"Layer: %@", NSStringFromClass([layer class])); + + if ([obj isKindOfClass:[CAShapeLayer class]]) + { + CAShapeLayer *shapeLayer = (CAShapeLayer *) obj; + NSNumber *lineIndexNumber = [shapeLayer valueForKey:@"indexTag"]; + NSInteger lineIndex = [lineIndexNumber integerValue]; + + if ((lineIndexNumber != nil) && (lineIndex > -1)) + { + CGPathRef path = [shapeLayer path]; + + CGPathApply(path, (__bridge void *)(dict), processPathElement); + + // if (CGPathContainsPoint(path, nil, point, YES)) { + NSNumber *didTouchNumber = dict[@"pointOnPath"]; + BOOL didTouchPoint = [didTouchNumber boolValue]; + + if (didTouchPoint) + { + // NSLog (@"Touch detected on Point at Shape Index: %i", indexTag); + NSInteger valueIndex = [self _indexForPointX:point.x]; // [valueIndexNumber integerValue]; + // NSLog (@"Touch detected on Point at Shape Index: %i", valueIndex); + + selectedDataPoint.lineIndex = lineIndex; + selectedDataPoint.valueIndex = valueIndex; + *stop = YES; + } + } + } + }]; + + [self maybeNotifyDelegateOfSelectionChangeFrom:_selectedDataPoint to:selectedDataPoint AtPoint:point]; +} + +- (void)maybeNotifyDelegateOfSelectionChangeFrom:(GKLineDataPoint *)previousSelection to:(GKLineDataPoint *)newSelection AtPoint:(CGPoint) targetPoint +{ + if (![previousSelection isEqual:newSelection]) + { + if (![previousSelection isEmptyDataPoint]) + { + [_delegate lineGraph:self willDeselectDataPoint:previousSelection AtPoint:targetPoint]; + } + + _selectedDataPoint = newSelection; + + if (![newSelection isEmptyDataPoint]) + { + [_delegate lineGraph:self willSelectDataPoint:newSelection AtPoint:targetPoint]; + + if (![previousSelection isEmptyDataPoint]) + { + [_delegate lineGraph:self didDeselectDataPoint:previousSelection AtPoint:targetPoint]; + } + + [_delegate lineGraph:self didSelectDataPoint:newSelection AtPoint:targetPoint]; + } else { + if (![previousSelection isEmptyDataPoint]) + { + [_delegate lineGraph:self didDeselectDataPoint:previousSelection AtPoint:targetPoint]; + } + } + } else + { + [_delegate lineGraph:self didReselectDataPoint:previousSelection AtPoint:targetPoint]; + } } @end From 49865c73175d42aa37b90a592bb9be8855b0c8a2 Mon Sep 17 00:00:00 2001 From: mr-brandt Date: Mon, 7 Jul 2014 22:48:26 +0200 Subject: [PATCH 2/2] - Added option to draw title labels at x-axis (not below the graph) - Added configurable axis tick length - Added fixed y-value interval - Added y-value tick marks --- Source/LineGraph/GKLineGraph.h | 2 + Source/LineGraph/GKLineGraph.m | 252 +++++++++++++++++++++++---------- 2 files changed, 182 insertions(+), 72 deletions(-) diff --git a/Source/LineGraph/GKLineGraph.h b/Source/LineGraph/GKLineGraph.h index 6d719ec..294d3aa 100755 --- a/Source/LineGraph/GKLineGraph.h +++ b/Source/LineGraph/GKLineGraph.h @@ -49,6 +49,7 @@ @property (nonatomic, assign) BOOL drawVerticalGridLines; @property (nonatomic, assign) BOOL drawHorizontalGridLines; @property (nonatomic, assign) NSInteger touchDistanceThreshold; +@property (nonatomic, assign) BOOL drawXAxisLabelsAtAxis; @property (nonatomic, strong) UIColor *coordinateSystemColor; @property (nonatomic, strong) UIColor *gridLinesColor; @@ -69,6 +70,7 @@ - (CFTimeInterval) animationDurationForLineAtIndex:(NSInteger)index; - (NSString *) titleForLineAtIndex:(NSInteger)index; +- (NSString *) identifierForLineAtIndex:(NSInteger)index; - (NSArray *) dashPatternForLineAtIndex: (NSInteger)index; @end diff --git a/Source/LineGraph/GKLineGraph.m b/Source/LineGraph/GKLineGraph.m index 985e57a..bf3e31c 100755 --- a/Source/LineGraph/GKLineGraph.m +++ b/Source/LineGraph/GKLineGraph.m @@ -34,12 +34,13 @@ static NSInteger kCoordinateLayerIndexTag = -1; static CGFloat kDefaultTouchDistanceThreshold = 5.0; +static NSInteger kXAxisTickLength = 5; static CGFloat kDefaultLineWidth = 3.0; static CGFloat kDefaultMargin = 10.0; static CGFloat kDefaultMarginBottom = 20.0; -static CGFloat kAxisMargin = 50.0; +static CGFloat kAxisMargin = 40.0; @interface GKLineGraph () @@ -87,10 +88,18 @@ - (void)draw { if ([self _hasTitleLabels]) [self _removeTitleLabels]; [self _constructTitleLabels]; - [self _positionTitleLabels]; - + + if (self.drawXAxisLabelsAtAxis) + { + [self _positionTitleLabelsNew]; + } else + { + [self _positionTitleLabels]; + } + if ([self _hasValueLabels]) [self _removeValueLabels]; - [self _constructValueLabels]; + // [self _constructValueLabels]; + [self _constructValueLabelsNew]; [self _drawLines]; } @@ -112,8 +121,8 @@ - (void)_constructTitleLabels { NSInteger count = [[self.dataSource valuesForLineAtIndex:0] count]; id items = [NSMutableArray arrayWithCapacity:count]; - for (NSInteger idx = 0; idx < count; idx++) { - + for (NSInteger idx = 0; idx < count; idx++) + { CGRect frame = CGRectMake(0, 0, kDefaultLabelWidth, kDefaultLabelHeight); UILabel *item = [[UILabel alloc] initWithFrame:frame]; item.textAlignment = NSTextAlignmentCenter; @@ -133,8 +142,8 @@ - (void)_removeTitleLabels { self.titleLabels = nil; } -- (void)_positionTitleLabels { - +- (void)_positionTitleLabels +{ __block NSInteger idx = 0; id values = [self.dataSource valuesForLineAtIndex:0]; [values mk_each:^(id value) { @@ -149,11 +158,41 @@ - (void)_positionTitleLabels { label.y = startY; [self addSubview:label]; - + idx++; }]; } +- (void)_positionTitleLabelsNew +{ + __block NSInteger idx = 0; + id values = [self.dataSource valuesForLineAtIndex:0]; + [values mk_each:^(id value) + { + CGFloat labelWidth = kDefaultLabelWidth; + CGFloat labelHeight = kDefaultLabelHeight; + CGFloat startX = [self _pointXForIndex:idx] - (labelWidth / 2); + + CGFloat startY = (self.height - labelHeight); + if ([self _maxValue] <= 0.0) + { + startY = [self _positionYForLineValue:0] + labelHeight; + } + if ([self _minValue] >= 0.0) + { + startY = [self _positionYForLineValue:0] - labelHeight - (kXAxisTickLength * [[UIScreen mainScreen] scale]) - 5; + } + + UILabel *label = [self.titleLabels objectAtIndex:idx]; + label.x = startX; + label.y = startY; + + [self addSubview:label]; + + idx++; + }]; +} + - (CGFloat)_pointXForIndex:(float)index { return kAxisMargin + self.margin + (index * [self _stepX]); } @@ -164,34 +203,86 @@ - (CGFloat)_indexForPointX:(float)pointX { } - (CGFloat)_stepX { - id values = [self.dataSource valuesForLineAtIndex:0]; + NSArray *values = [self.dataSource valuesForLineAtIndex:0]; CGFloat result = ([self _plotWidth] / [values count]); return result; } -- (void)_constructValueLabels { - +- (void)_constructValueLabels +{ NSInteger count = self.valueLabelCount; - id items = [NSMutableArray arrayWithCapacity:count]; + NSMutableArray *items = [NSMutableArray arrayWithCapacity:count]; - for (NSInteger idx = 0; idx < count; idx++) { - + for (NSInteger idx = 0; idx < count; idx++) + { CGRect frame = CGRectMake(0, 0, kDefaultLabelWidth, kDefaultLabelHeight); UILabel *item = [[UILabel alloc] initWithFrame:frame]; item.textAlignment = NSTextAlignmentRight; item.font = [UIFont boldSystemFontOfSize:12]; item.textColor = [UIColor lightGrayColor]; - + CGFloat value = [self _minValue] + (idx * [self _stepValueLabelY]); item.centerY = [self _positionYForLineValue:value]; item.text = [@(ceil(value)) stringValue]; -// item.text = [@(value) stringValue]; + // item.text = [@(value) stringValue]; + + [items addObject:item]; + [self addSubview:item]; + } + self.valueLabels = items; +} + +- (void)_constructValueLabelsNew +{ + NSInteger maxDistance = MAX ([self _maxValue], fabs([self _minValue])); + NSInteger stepSize = maxDistance / 10; + stepSize = 5 * floor((stepSize/5)+0.5); + + NSMutableArray *items = [NSMutableArray arrayWithCapacity:5]; + + NSInteger currentStepValue = 0; + while (currentStepValue <= [self _maxValue]) + { + CGRect frame = CGRectMake(0, 0, kDefaultLabelWidth, kDefaultLabelHeight); + UILabel *item = [[UILabel alloc] initWithFrame:frame]; + item.textAlignment = NSTextAlignmentRight; + item.font = [UIFont boldSystemFontOfSize:12]; + item.textColor = [UIColor lightGrayColor]; + + item.centerY = [self _positionYForLineValue:currentStepValue]; + + item.text = [@(ceil(currentStepValue)) stringValue]; + // item.text = [@(value) stringValue]; + + [items addObject:item]; + [self addSubview:item]; + + currentStepValue += stepSize; + } + + currentStepValue = 0; + while (currentStepValue >= [self _minValue]) + { + CGRect frame = CGRectMake(0, 0, kDefaultLabelWidth, kDefaultLabelHeight); + UILabel *item = [[UILabel alloc] initWithFrame:frame]; + item.textAlignment = NSTextAlignmentRight; + item.font = [UIFont boldSystemFontOfSize:12]; + item.textColor = [UIColor lightGrayColor]; + + item.centerY = [self _positionYForLineValue:currentStepValue]; + + item.text = [@(ceil(currentStepValue)) stringValue]; + // item.text = [@(value) stringValue]; [items addObject:item]; [self addSubview:item]; + + currentStepValue -= stepSize; } + self.valueLabels = items; + self.valueLabelCount = [items count]; } - (CGFloat)_stepValueLabelY { @@ -242,7 +333,7 @@ - (void)_drawLines { NSInteger numValues = 0; for (NSInteger idx = 0; idx < [self.dataSource numberOfLines]; idx++) { numValues = MAX(numValues, [[self.dataSource valuesForLineAtIndex:idx] count]); - + [self _drawLineAtIndex:idx]; } @@ -279,7 +370,7 @@ - (void)_drawHorizontalGridLines:(NSInteger) indexCount { UILabel *label = [self.valueLabels objectAtIndex:idx]; CGFloat yCoordinate = label.frame.origin.y + (label.frame.size.height / 2); // NSLog(@"yValue: %f", yValue); - + [path moveToPoint:CGPointMake([self _pointXForIndex:0], yCoordinate)]; [path addLineToPoint:CGPointMake([self _pointXForIndex:0], yCoordinate)]; [path addLineToPoint:CGPointMake([self _pointXForIndex:indexCount], yCoordinate)]; @@ -348,7 +439,7 @@ - (void)_drawCoordinateSystem:(NSInteger) indexCount { CGFloat x = [self _pointXForIndex:0]; CGFloat y = [self _positionYForLineValue:[self _minValue]]; CGPoint point = CGPointMake(x, y); - + [path moveToPoint:point]; [path addLineToPoint:point]; @@ -365,24 +456,37 @@ - (void)_drawCoordinateSystem:(NSInteger) indexCount { [path moveToPoint:point]; [path addLineToPoint:point]; - + x = [self _pointXForIndex:indexCount]; point = CGPointMake(x, y); [path addLineToPoint:point]; - + // Draw x Copordinate Caps - float maxCapValue = 0.03125 * MAX([self _maxValue], fabs([self _minValue])); - float minCapValue = -1 * maxCapValue; - - for (int step = 1; step < indexCount; step++) + for (int step = 0; step < indexCount; step++) { - [path moveToPoint:CGPointMake([self _pointXForIndex:step], [self _positionYForLineValue:maxCapValue])]; - [path addLineToPoint:CGPointMake([self _pointXForIndex:step], [self _positionYForLineValue:maxCapValue])]; - [path addLineToPoint:CGPointMake([self _pointXForIndex:step], [self _positionYForLineValue:minCapValue])]; + CGFloat baseYCoordinate = [self _positionYForLineValue:0]; + [path moveToPoint:CGPointMake([self _pointXForIndex:step], baseYCoordinate - (kXAxisTickLength * [[UIScreen mainScreen] scale]))]; + [path addLineToPoint:CGPointMake([self _pointXForIndex:step], baseYCoordinate - (kXAxisTickLength * [[UIScreen mainScreen] scale]))]; + [path addLineToPoint:CGPointMake([self _pointXForIndex:step], baseYCoordinate + (kXAxisTickLength * [[UIScreen mainScreen] scale]))]; // bezierPath.lineJoinStyle = kCGLineJoinRound; } + // Draw y coordinate caps + NSInteger count = self.valueLabelCount; + for (NSInteger idx = 0; idx < count; idx++) + { + UILabel *label = [self.valueLabels objectAtIndex:idx]; + CGFloat yCoordinate = label.frame.origin.y + (label.frame.size.height / 2); + // NSLog(@"yValue: %f", yValue); + CGFloat left = [self _pointXForIndex:0] - (kXAxisTickLength * [[UIScreen mainScreen] scale]); + CGFloat right = [self _pointXForIndex:0] + (kXAxisTickLength * [[UIScreen mainScreen] scale]); + + [path moveToPoint:CGPointMake(left, yCoordinate)]; + [path addLineToPoint:CGPointMake(left, yCoordinate)]; + [path addLineToPoint:CGPointMake(right, yCoordinate)]; + } + layer.path = path.CGPath; if (self.animated) { @@ -409,6 +513,11 @@ - (void)_drawLineAtIndex:(NSInteger)index { NSArray *dashPattern = [self.dataSource dashPatternForLineAtIndex:index]; [layer setLineDashPattern:dashPattern]; } + if ([_dataSource respondsToSelector:@selector(identifierForLineAtIndex:)]) + { + NSString *lineIdentifier = [self.dataSource identifierForLineAtIndex:index]; + [layer setValue:lineIdentifier forKey:@"identifier"]; + } [layer setValue:[NSNumber numberWithInteger:index] forKey:@"indexTag"]; @@ -419,7 +528,6 @@ - (void)_drawLineAtIndex:(NSInteger)index { NSInteger idx = 0; id values = [self.dataSource valuesForLineAtIndex:index]; for (id item in values) { - CGFloat x = [self _pointXForIndex:idx]; CGFloat y = [self _positionYForLineValue:[item floatValue]]; CGPoint point = CGPointMake(x, y); @@ -467,7 +575,7 @@ - (CAShapeLayer *)_layerWithPath:(UIBezierPath *)path { item.lineCap = kCALineCapRound; item.lineJoin = kCALineJoinRound; item.lineWidth = self.lineWidth; -// item.strokeColor = [self.foregroundColor CGColor]; + // item.strokeColor = [self.foregroundColor CGColor]; item.strokeColor = [[UIColor redColor] CGColor]; item.strokeEnd = 1; return item; @@ -479,14 +587,14 @@ - (CABasicAnimation *)_animationWithKeyPath:(NSString *)keyPath { animation.duration = self.animationDuration; animation.fromValue = @(0); animation.toValue = @(1); -// animation.delegate = self; + // animation.delegate = self; return animation; } - (void)reset { - self.layer.sublayers = nil; [self _removeTitleLabels]; [self _removeValueLabels]; + self.layer.sublayers = nil; self.selectedDataPoint = [GKLineDataPoint new]; } @@ -503,17 +611,17 @@ void processPathElement(void* info, const CGPathElement* element) if (element->type == kCGPathElementMoveToPoint) { NSMutableDictionary *theDict = (__bridge NSMutableDictionary *) info; - + NSValue *pointValue = theDict[@"thePoint"]; CGPoint touchedPoint = [pointValue CGPointValue]; - + CGFloat xDist = (touchedPoint.x - pointArg.x); CGFloat yDist = (touchedPoint.y - pointArg.y); distance = sqrt((xDist * xDist) + (yDist * yDist)); NSNumber *thresholdNumber = (NSNumber*)theDict[@"touchThreshold"]; NSInteger touchDistanceThreshold = [thresholdNumber integerValue]; - + if (distance <= touchDistanceThreshold) { theDict[@"pointOnPath"] = [NSNumber numberWithBool:YES]; @@ -539,39 +647,39 @@ - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event GKLineDataPoint *selectedDataPoint = [GKLineDataPoint new]; [subLayers enumerateObjectsUsingBlock:^(id obj, NSUInteger index, BOOL *stop) - { - // CALayer *layer = (CALayer *) obj; - // NSLog (@"Layer: %@", NSStringFromClass([layer class])); - - if ([obj isKindOfClass:[CAShapeLayer class]]) - { - CAShapeLayer *shapeLayer = (CAShapeLayer *) obj; - NSNumber *lineIndexNumber = [shapeLayer valueForKey:@"indexTag"]; - NSInteger lineIndex = [lineIndexNumber integerValue]; - - if ((lineIndexNumber != nil) && (lineIndex > -1)) - { - CGPathRef path = [shapeLayer path]; - - CGPathApply(path, (__bridge void *)(dict), processPathElement); - - // if (CGPathContainsPoint(path, nil, point, YES)) { - NSNumber *didTouchNumber = dict[@"pointOnPath"]; - BOOL didTouchPoint = [didTouchNumber boolValue]; - - if (didTouchPoint) - { - // NSLog (@"Touch detected on Point at Shape Index: %i", indexTag); - NSInteger valueIndex = [self _indexForPointX:point.x]; // [valueIndexNumber integerValue]; - // NSLog (@"Touch detected on Point at Shape Index: %i", valueIndex); - - selectedDataPoint.lineIndex = lineIndex; - selectedDataPoint.valueIndex = valueIndex; - *stop = YES; - } - } - } - }]; + { + // CALayer *layer = (CALayer *) obj; + // NSLog (@"Layer: %@", NSStringFromClass([layer class])); + + if ([obj isKindOfClass:[CAShapeLayer class]]) + { + CAShapeLayer *shapeLayer = (CAShapeLayer *) obj; + NSNumber *lineIndexNumber = [shapeLayer valueForKey:@"indexTag"]; + NSInteger lineIndex = [lineIndexNumber integerValue]; + + if ((lineIndexNumber != nil) && (lineIndex > -1)) + { + CGPathRef path = [shapeLayer path]; + + CGPathApply(path, (__bridge void *)(dict), processPathElement); + + // if (CGPathContainsPoint(path, nil, point, YES)) { + NSNumber *didTouchNumber = dict[@"pointOnPath"]; + BOOL didTouchPoint = [didTouchNumber boolValue]; + + if (didTouchPoint) + { + // NSLog (@"Touch detected on Point at Shape Index: %i", indexTag); + NSInteger valueIndex = [self _indexForPointX:point.x]; // [valueIndexNumber integerValue]; + // NSLog (@"Touch detected on Point at Shape Index: %i", valueIndex); + + selectedDataPoint.lineIndex = lineIndex; + selectedDataPoint.valueIndex = valueIndex; + *stop = YES; + } + } + } + }]; [self maybeNotifyDelegateOfSelectionChangeFrom:_selectedDataPoint to:selectedDataPoint AtPoint:point]; } @@ -584,13 +692,13 @@ - (void)maybeNotifyDelegateOfSelectionChangeFrom:(GKLineDataPoint *)previousSele { [_delegate lineGraph:self willDeselectDataPoint:previousSelection AtPoint:targetPoint]; } - + _selectedDataPoint = newSelection; - + if (![newSelection isEmptyDataPoint]) { [_delegate lineGraph:self willSelectDataPoint:newSelection AtPoint:targetPoint]; - + if (![previousSelection isEmptyDataPoint]) { [_delegate lineGraph:self didDeselectDataPoint:previousSelection AtPoint:targetPoint];