Using core-plot to draw line graphs

03 Oct

Using core-plot to draw line graphs

I’ve not seen many advanced examples of graphs being created using the core-plot framework so I thought I’d post one.

The below code produces this screen:

advanced core-plot graph

advanced core-plot graph

Please note that in the below code, I’ve not included any of my datamodel classes.

.h:


#import <UIKit/UIKit.h>
#import "CorePlot-CocoaTouch.h"

@interface CostPerMileGraphController : UIViewController <CPPlotDataSource>{
	CPXYGraph *graph;
	NSArray *points;
	int pointsCountMinusStartingPoint;
	int intYNumberOfCharacters;
}

@property(nonatomic, retain) NSArray *points;
@property(nonatomic, assign) int pointsCountMinusStartingPoint;
@property(nonatomic, assign) int intYNumberOfCharacters;

@end

Things to note in the above is that I’m setting up my class to conform the CPPlotDatasource, creating an instance variable for my graph and adding an array which will store all the points to be plotted on the graph.

.m:


#import "CostPerMileGraphController.h"
#import "Vehicle.h"
#import "Mileage.h"
#import "DataModel.h"

static int const kGraphPadding = 1.0;
static int const kSampleRate = 20;
static int const kNumberOfReadingToDisplay = 10;
static int const kPaddingForYTitle = 20;
static int const kCharacterPadding = 12;


@interface CostPerMileGraphController(Private)

/*
 @method Fills the points array with values
 */
-(NSArray*)loadPoints;

/*
 @method controls the position of the graph on the view
 */
-(void)positionGraph:(CPXYGraph*)graphPosition;

/*
 @method controls the position of the plot with the graph
 */
-(void)positionPlotArea:(CPPlotAreaFrame*)plotArea;

/*
 @method Sets up the X axis on a graph
 */
-(void)configureXAxisForGraph:(CPXYGraph*)graphForAxis;

/*
 @method Sets up the X axis on a graph
 */
-(void)configureYAxisForGraph:(CPXYGraph*)graphForAxis;

/*
 @method sets the properties of the line
 */
-(void)linePropertiesForPlot:(CPScatterPlot*)plot;

/*
 @method returns the top value in the value array with an offset
 */
-(float)getYTopValueWithOffset:(float)offset;

/*
 @method returns the bottom value in the value array with an offset
 */
-(float)getYBottomValueWithOffset:(float)offset;


/*
 @method returns the cost per mile
 */
-(NSNumber*)getCostPerMile:(NSNumber*)milesTravelled cost:(NSNumber*)cost;

/*
 @method pushes the next view controller onto the navigation stack
 */
- (void) loadNextViewController;

@end


@implementation CostPerMileGraphController

@synthesize points;
@synthesize pointsCountMinusStartingPoint;
@synthesize intYNumberOfCharacters;


#pragma mark -
#pragma mark Setup Methods

// Implement viewDidLoad to do additional setup after loading the synthesize view, typically from a nib.
- (void)viewDidLoad {
    [super viewDidLoad];
	self.navigationItem.title= NSLocalizedString(@"COST_PER_MILE", @"");
	self.navigationItem.hidesBackButton = FALSE;


	self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"NEXT", @"") style:UIBarButtonItemStylePlain target:self action:@selector(loadNextViewController)];


	if (self.points == nil) {
		self.points = [self loadPoints];
	}

	if (self.pointsCountMinusStartingPoint == 0) {
		self.pointsCountMinusStartingPoint = ([self.points count]-1);
	}

	graph = [[CPXYGraph alloc] initWithFrame: self.view.bounds];

	[self positionGraph:graph];

	CPLayerHostingView *hostingView;
	for (UIView *subview in self.view.subviews) {
		hostingView = (CPLayerHostingView*)subview;
		break;
	}
	hostingView.hostedLayer = graph;

	[graph applyTheme:[CPTheme themeNamed:kCPSlateTheme]];

	CPXYPlotSpace *plotSpace = (CPXYPlotSpace *)graph.defaultPlotSpace;
	plotSpace.xRange = [CPPlotRange plotRangeWithLocation:CPDecimalFromFloat(0) length:CPDecimalFromFloat([self.points count])];
	plotSpace.yRange = [CPPlotRange plotRangeWithLocation:CPDecimalFromFloat(0) length:CPDecimalFromFloat([self getYTopValueWithOffset:([self getYTopValueWithOffset:0]/kSampleRate)])];

	[self configureXAxisForGraph:graph];
	[self configureYAxisForGraph:graph];

	CPScatterPlot *plot = [[[CPScatterPlot alloc] initWithFrame:graph.bounds] autorelease];
	plot.identifier = @"CostPerMile";
	plot.dataSource = self;

	[self positionPlotArea:graph.plotAreaFrame];
	[graph addPlot:plot];
	[self linePropertiesForPlot:plot];

}


#pragma mark -
#pragma mark loading methods

-(void) loadNextViewController{
	//Load next view controller
}


#pragma mark -
#pragma mark LineProperties

-(void)linePropertiesForPlot:(CPScatterPlot*)plot{

	plot.dataLineStyle.lineWidth = 2.0f;
	plot.dataLineStyle.lineColor = [CPColor blackColor];

	CPColor *topColour = [CPColor colorWithComponentRed:(247.0/255.0) green:(209.0/255.0) blue:(23.0/255.0) alpha:0.85];
	CPColor *bottomColour = [CPColor colorWithComponentRed:(247.0/255.0) green:(209.0/255.0) blue:(23.0/255.0) alpha:0.85];

	CPGradient *areaGradient = [CPGradient gradientWithBeginningColor:bottomColour endingColor:topColour];
	areaGradient.angle = 90.0f;
	CPFill *areaGradientFill = [CPFill fillWithGradient:areaGradient];
	plot.areaFill = areaGradientFill;
	plot.areaBaseValue = CPDecimalFromFloat([self getYBottomValueWithOffset:0]);
}

#pragma mark -
#pragma mark Range value methods


-(float)getYTopValueWithOffset:(float)offset{
	if (self.points == nil) {
		self.points = [self loadPoints];
	}

	float value = 0;

	for (int x = 0; x < pointsCountMinusStartingPoint; x++) {

		float compareValue = [[self getCostPerMile:((Mileage*)[self.points objectAtIndex:(x+1)]).dblDiffSinceLastMileage cost:((Mileage*)[self.points objectAtIndex:(x)]).dblFuelCost] floatValue];

		if (x == 0) {
			value = compareValue;
		}else {
			if (compareValue > value) {
				value = compareValue;
			}
		}
	}
	return (value + offset);
}


-(float)getYBottomValueWithOffset:(float)offset{
	return 0.0;
}

#pragma mark -
#pragma mark Positioning methods

-(void)positionGraph:(CPXYGraph*)graphPosition{
	graphPosition.paddingBottom = kGraphPadding;
	graphPosition.paddingTop = kGraphPadding;
	graphPosition.paddingLeft = kGraphPadding;
	graphPosition.paddingRight = kGraphPadding;
}

-(void)positionPlotArea:(CPPlotAreaFrame*)plotArea{
	plotArea.paddingLeft = (kCharacterPadding*self.intYNumberOfCharacters) + kPaddingForYTitle;
	plotArea.paddingRight = 3;
	plotArea.paddingBottom = 70;
	plotArea.paddingTop = 25;
}

#pragma mark -
#pragma mark Axis methods

-(void)configureXAxisForGraph:(CPXYGraph*)graphForAxis{

	CPXYAxisSet *axisSet = (CPXYAxisSet *)graphForAxis.axisSet;
    CPXYAxis *x = axisSet.xAxis;
    x.minorTicksPerInterval = 0;
	x.labelOffset = 0.0f;
	x.labelingPolicy = CPAxisLabelingPolicyNone;
	//set the starting position of the y-axis
 	x.orthogonalCoordinateDecimal = CPDecimalFromFloat(0);
	x.title = NSLocalizedString(@"DATE", @"");
	x.titleOffset = 50.0;

	NSMutableArray *customTickLocations = [[NSMutableArray alloc] initWithCapacity:self.pointsCountMinusStartingPoint];

	for (int i = 0; i < self.pointsCountMinusStartingPoint; i++) {
		[customTickLocations addObject:[NSNumber numberWithInt:i]];
	}

	NSMutableArray *customLabels = [[NSMutableArray alloc] initWithCapacity: customTickLocations.count];

	for (int i = 0; i < [customTickLocations count]; i++) {

		NSNumber *tickLocation = [customTickLocations objectAtIndex:i];

		NSString *date = nil;
		NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
		Mileage *reading = (Mileage*)[self.points objectAtIndex:(i+1)];
		[formatter setDateFormat:@"dd/MM/YY"];
		date = [formatter stringFromDate:reading.dteDate];

		CPAxisLabel *newLabel = [[CPAxisLabel alloc] initWithText:date textStyle:x.labelTextStyle];

		newLabel.tickLocation = [tickLocation decimalValue];
		newLabel.rotation = M_PI/4;
		newLabel.offset = x.labelOffset + x.majorTickLength;
		[customLabels addObject:newLabel];
		[newLabel release];
		[formatter release];
	}

	x.axisLabels =  [NSSet setWithArray:customLabels];
	x.majorTickLocations =  [NSSet setWithArray:customTickLocations];
	[customTickLocations release];
	[customLabels release];
	x.masksToBorder = YES;

}

-(void)configureYAxisForGraph:(CPXYGraph*)graphForAxis{

	CPXYAxisSet *axisSet = (CPXYAxisSet *)graphForAxis.axisSet;
    CPXYAxis *y = axisSet.yAxis;
    y.minorTicksPerInterval = 0;
	y.labelOffset = 0.0f;
	y.labelingPolicy = CPAxisLabelingPolicyNone;
	//set the starting position of the x-axis
	y.orthogonalCoordinateDecimal = CPDecimalFromString(@"0");
	y.title = NSLocalizedString(@"PRICE", @"");

	int yRange = [self getYTopValueWithOffset:0] - [self getYBottomValueWithOffset:0];


	NSMutableArray *customTickLocations = [[NSMutableArray alloc] initWithCapacity:yRange];

	float sample = ([self getYTopValueWithOffset:0] - [self getYBottomValueWithOffset:0])/kSampleRate;

	for (int x = 0; x < (kSampleRate + 1); x++) {
		[customTickLocations addObject:[NSNumber numberWithFloat:((x*sample) +  [self getYBottomValueWithOffset:0])]];
	}

	NSMutableArray *customLabels = [[NSMutableArray alloc] initWithCapacity: customTickLocations.count];

	for (int i = 0; i < [customTickLocations count]; i++) {

		NSNumber *tickLocation = [customTickLocations objectAtIndex:i];

		//only show every second label
		if (i % 2 == 0) {

			NSString* formattedNumber = [NSString stringWithFormat:@"%.01f", [tickLocation floatValue]];

			CPAxisLabel *newLabel = [[CPAxisLabel alloc] initWithText:formattedNumber textStyle:y.labelTextStyle];

			newLabel.tickLocation = [tickLocation decimalValue];
			newLabel.offset = y.labelOffset + y.majorTickLength;
			[customLabels addObject:newLabel];
			[newLabel release];
		}
	}

	float number = [[customTickLocations objectAtIndex:([customTickLocations count] -1)] floatValue];
	NSString *strRepresentation = [NSString stringWithFormat:@"%.01f", number];
	self.intYNumberOfCharacters = [strRepresentation length];

	y.titleOffset = (kCharacterPadding*self.intYNumberOfCharacters);

	y.alternatingBandFills = [NSArray arrayWithObjects:[[CPColor whiteColor] colorWithAlphaComponent:0.2], [NSNull null], nil];
	y.axisLabels =  [NSSet setWithArray:customLabels];
	y.majorTickLocations =  [NSSet setWithArray:customTickLocations];
	y.masksToBorder = YES;
	[customLabels release];
	[customTickLocations release];
}


#pragma mark -
#pragma mark setup methods

-(NSArray*)loadPoints{

	Vehicle *vehicle = [DataModel theDataModel].currentVehicle;

	NSArray *readings = (NSArray*)vehicle.rVehicleMileage;

	NSMutableArray* newestReadings = [[NSMutableArray alloc] initWithCapacity:kNumberOfReadingToDisplay];

	if ([readings count] > kNumberOfReadingToDisplay) {
		NSSortDescriptor *newestFirst = [[[NSSortDescriptor alloc] initWithKey:@"dteDate" ascending:NO] autorelease];

		readings = [readings sortedArrayUsingDescriptors:[NSArray arrayWithObject:newestFirst]];

		for (int x = 0; x <= kNumberOfReadingToDisplay; x++) {
			[newestReadings addObject:[readings objectAtIndex:x]];
		}

		NSSortDescriptor *oldestFirst = [[[NSSortDescriptor alloc] initWithKey:@"dteDate" ascending:YES] autorelease];

		readings = [(NSArray*)newestReadings sortedArrayUsingDescriptors:[NSArray arrayWithObject:oldestFirst]];


	}else {
		NSSortDescriptor *descriptor = [[[NSSortDescriptor alloc] initWithKey:@"dteDate" ascending:YES] autorelease];

		readings = [readings sortedArrayUsingDescriptors:[NSArray arrayWithObject:descriptor]];
	}

	[newestReadings release];

	return readings;
}

#pragma mark -
#pragma mark CPPlotDataSource methods

- (NSUInteger)numberOfRecordsForPlot:(CPPlot *)plot {

	if (self.points == nil) {
		self.points = [self loadPoints];
	}

	return pointsCountMinusStartingPoint;
}

- (NSNumber *)numberForPlot:(CPPlot *)plot field:(NSUInteger)fieldEnum recordIndex:(NSUInteger)index {

	NSNumber* returnValue = nil;

	if (fieldEnum == CPScatterPlotFieldX) {
		returnValue = [NSNumber numberWithDouble:index];
	}else {
		returnValue = [self getCostPerMile:((Mileage*)[self.points objectAtIndex:(index+1)]).dblDiffSinceLastMileage cost:((Mileage*)[self.points objectAtIndex:(index)]).dblFuelCost];
	}

	return returnValue;
}

#pragma mark -
#pragma mark Formula

-(NSNumber*)getCostPerMile:(NSNumber*)milesTravelled cost:(NSNumber*)cost{

	if ([milesTravelled floatValue] == 0.0) {
		return [NSNumber numberWithInt: 0];
	}else {
		return [NSNumber numberWithFloat:([milesTravelled floatValue]/[cost floatValue])];
	}
}

#pragma mark -
#pragma mark Memory management

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];

}

- (void)viewDidUnload {
    [super viewDidUnload];
}


- (void)dealloc {
    [super dealloc];
	[self.points release];
	[graph release];
}


@end

Ok so lets get an understanding of that the above is doing.


#import "Vehicle.h"
#import "Mileage.h"
#import "DataModel.h"
#import "SummaryController.h"

static CGFloat const kGraphPadding = 1.0;
static int const kSampleRate = 20;
static int const kNumberOfReadingToDisplay = 10;
static int const kPaddingForYTitle = 20;
static int const kCharacterPadding = 12;

Here we're importing the classes that I need for my class to retrieve the data that will be used to populate the points array. The constants are used because these values are used multiple times throughout the code and it makes sense to define these only once.


@interface CostPerMileGraphController(Private)

/*
 @method Fills the points array with values
 */
-(NSArray*)loadPoints;

/*
 @method controls the position of the graph on the view
 */
-(void)positionGraph:(CPXYGraph*)graphPosition;

/*
 @method controls the position of the plot with the graph
 */
-(void)positionPlotArea:(CPPlotAreaFrame*)plotArea;

/*
 @method Sets up the X axis on a graph
 */
-(void)configureXAxisForGraph:(CPXYGraph*)graphForAxis;

/*
 @method Sets up the X axis on a graph
 */
-(void)configureYAxisForGraph:(CPXYGraph*)graphForAxis;

/*
 @method sets the properties of the line
 */
-(void)linePropertiesForPlot:(CPScatterPlot*)plot;

/*
 @method returns the top value in the value array with an offset
 */
-(float)getYTopValueWithOffset:(float)offset;

/*
 @method returns the bottom value in the value array with an offset
 */
-(float)getYBottomValueWithOffset:(float)offset;


/*
 @method returns the cost per mile
 */
-(NSNumber*)getCostPerMile:(NSNumber*)milesTravelled cost:(NSNumber*)cost;

@end

Here we're using the extension feature of Objective-C to provide provide more methods for our class to use but without making these methods visible from outside the class. We're talk more about these methods when we come to them in the code.


- (void)viewDidLoad {
    [super viewDidLoad];
	self.navigationItem.title= NSLocalizedString(@"COST_PER_MILE", @"");
	self.navigationItem.hidesBackButton = FALSE;

	self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"NEXT", @"") style:UIBarButtonItemStylePlain target:self action:@selector(loadSummaryView)];

	if (self.points == nil) {
		self.points = [self loadPoints];
	}

	if (self.pointsCountMinusStartingPoint == 0) {
		self.pointsCountMinusStartingPoint = ([self.points count]-1);
	}

	graph = [[CPXYGraph alloc] initWithFrame: self.view.bounds];

	[self positionGraph:graph];

	CPLayerHostingView *hostingView;
	for (UIView *subview in self.view.subviews) {
		hostingView = (CPLayerHostingView*)subview;
		break;
	}
	hostingView.hostedLayer = graph;

	[graph applyTheme:[CPTheme themeNamed:kCPSlateTheme]];

	CPXYPlotSpace *plotSpace = (CPXYPlotSpace *)graph.defaultPlotSpace;
	plotSpace.xRange = [CPPlotRange plotRangeWithLocation:CPDecimalFromFloat(0) length:CPDecimalFromFloat([self.points count])];
	plotSpace.yRange = [CPPlotRange plotRangeWithLocation:CPDecimalFromFloat(0) length:CPDecimalFromFloat([self getYTopValueWithOffset:([self getYTopValueWithOffset:0]/kSampleRate)])];

	[self configureXAxisForGraph:graph];
	[self configureYAxisForGraph:graph];

	CPScatterPlot *plot = [[[CPScatterPlot alloc] initWithFrame:graph.bounds] autorelease];
	plot.identifier = @"CostPerMile";
	plot.dataSource = self;

	[self positionPlotArea:graph.plotAreaFrame];
	[graph addPlot:plot];
	[self linePropertiesForPlot:plot];

}

So there is a lot happening here. We're skip down to line 16 as most of whats happening above that shouldn't be new to you.

Line 16, so on this line with create a CPXYGraph, giving it the frame size of our view and assigning it to the graph that we created in our .h file.

Line 18, we call our first extension method, this effectively just pads the graph based on the values we gave in our defines

Lines 20 - 25, we loop through all our subviews (there is only the one) and use this subview as the holder for our graph. This is needed as the graph must sit inside a "CPLayerHostingView" which IB will moan about not knowing if we don't do it this way.

Line 27, setting a theme for our graph

Line 29-34, here we dynamically work out the range of our x and y points and configure our axes to fit this range. This is did using more extension methods. In this method we work out what the top x and y values are and then add some more on to make it more pleasing to the eye.

Line 36-38, here we create the line plot (or graph proper), give it a name and assign its delegate datasource to our own class.

Line 40-42, here we adjust the plot area with padding, add it to the graph and call another extension to set the properties of the line, in this case its width, colour and the colour of the fill between line and axes.

Everything feeds off of this method and the methods are pretty self evident.

24 thoughts on “Using core-plot to draw line graphs

  1. William, I just wanted to thank you for posting your code of such a detailed and customized core plot example. I actually didn’t find it in my earlier research because I was initially working on bar graphs but I’m glad I did find it now it’s really helped my progress. Thanks!

  2. Great tutorial!

    I ‘m having some problems with it. Do you have the sample files post somewhere?

    Thank you for posting the tutorial. Is the best on core-plot!

    • Hi Mona, sadly I can’t find the example project that the above code is based on. However I should have some spare time soon and I’ll re-create it (gives me a chance to tidy it up and improve the above tutorial)

  3. Thank you William.

    I got partially working graph.

    X date data like: 01/05/10, 01/07/10, 01/08/10 …etc. they could be any date on a month and not necessary every day of the month.

    Y data like are minutes: 40, 35, 44, 44, 45, 44, 44, 44, 47, 50… etc could be any number between 0 and +- 120

    I want to have the graph showing:
    X: all the dates on my array. I don’t want all the days on a month.
    Y : only a range from the lower number like 35 from array above to the top number like 50 from array above.

    Right now my graph shows with the changes I make to your code:
    X: all the dates as I need
    Y: a range from 0 to my top number like 50 above.

    I try changing plotSpace.yRange (from low Number to Top number) to show only the range of y data on my array but it doesn’t work. The dates will no longer show if I change the range.

    Do you have any suggestions to making the graph just show the a range of Y data from my array (like 35 to 50) and not from 0?

    Thanks again!

  4. Thanks for great tutorial,

    i have x axis with time and y with values and i want to draw more than 1 line with different colors. is it possible with core plot

    thanks once again

    • Sorry Pooja am not sure and I wasn’t able to find anything on this either :-(

      I guess if its not supported I would be tempted to lay one graph on top of each other, each showing a different line that would appear to the end user as one graph. A bit hack-y but may work for you.

      • William can you give me an idea how to implement one graph on top of each other.
        I try to Google it but not succeeded
        Thank you once again

        • Hi Pooja,

          Creating another plotSpace and adding it to your graph should do the trick:

          CPXYPlotSpace *plotSpaceA = [[[CPXYPlotSpace alloc] init] autorelease];
          [graph addPlotSpace:plotSpaceA];
          CPXYPlotSpace *plotSpaceB = [[[CPXYPlotSpace alloc] init] autorelease];
          [graph addPlotSpace:plotSpaceB];

  5. William,

    I just finished my graph. It’s working just as I want it with scrolling.

    I want to thank you for sharing your code. If it wasn’t for your code I’ll probably still lost in the world of coreplot. I learned a lot from your code.

    Thank again!

  6. Hi Williams,

    It’s great tutorial.i had seen the discussion between MONA and U can I have sample running project friend.
    Thanks

    • Sorry SMDR, I never got round to recreating that project. I am currently working on a new project that uses CorePlot and when I get that up and running, I’ll email the code over to you

  7. Hi,
    First of all, thanks for your great tutorial. I’ve just started with this coreplot, and I’ve got a problem. I just wanted to change the color of the line. And I’ve tried; dataSourceLinePlot.dataLineStyle.lineColor=[CPColor whiteColor]; and

    dataSourceLinePlot.dataLineStyle.lineColor = [CPColor colorWithComponentRed:0.0 green:0.0 blue:205.0/255.0 alpha:1.0];

    in both cases I’m getting an error: “Object cannot set – either readonly property or no setter found.”
    I don’t know what to do. Please help me….

    • Hi Mithun,

      That is a tricky one but I think I managed to track it down. It looks as if they have changed that functionality since I wrote this article but I think I’ve found a solution that am using in a current project am working on.

      Are you assigning directly to the default dataLineStyle.lineColor object?

      If so I believe the below code snippet may work for you (I’ve included some other code to give it some context):

      CPScatterPlot *boundLinePlot = [[[CPScatterPlot alloc] init] autorelease];
      CPMutableLineStyle *lineStyle = [CPMutableLineStyle lineStyle];
      lineStyle.lineWidth = 3.0f;
      lineStyle.lineColor = [CPColor blueColor];
      boundLinePlot.dataLineStyle = lineStyle;
      boundLinePlot.identifier = @”Your plot name”;
      boundLinePlot.dataSource = self;
      [_graph addPlot:boundLinePlot];

      In the above I am creating a scatterplot but whats really of interest to you is the “CPMutableLineStyle” allowing me to set its properties including the lineColor! I then assign the lineStyle object tot he scatterplot.

      Hope that helps

  8. Hi everyone as i am new to iphone i am so what confused to understand this.please anyone can send me the code for the above sample.

  9. Hi Everyone,

    I want a line graph but the x,y values are coming from sqlite database , I got two arrays which are retrieved by sqlite ,but i don’t know how to set those arrays as x,y values, can any one help me?..

    Murali

  10. Hi,

    How to create a offset for scatterplot? i have created barplot with some offset from y axis. In the same way, I want to create scatter plot with some point from y axis

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

  • Facebook
  • Twitter