When working with data intensive applications, a developer must often do more than just show lists of data records in a table view. The CorePlot library will allow you to add stunning data visualizations to your applications. Find out how in this Tuts+ Premium series!
Also available in this series:
- Working With CorePlot: Project Setup
- Working with CorePlot: Plot Fundamentals
- Working with CorePlot: Styling and Adding Plots
- Working with CorePlot: Creating a Bar Chart
- Working with CorePlot: Creating a Pie Chart
Where We Left Off
Last time we got into how to create a bar chart, how to customize the bar colors, and how to add custom labels to our axis if we want to display custom text instead of just numbers.
What We'll Cover Today
This time we'll cover how to abstract the logic out of our controller into a more reusable data source object. Once we've mastered this, we'll go over how to display the same data as the bar chart, except this time displaying it as a pie chart!
Step 1: Setting Up
Before we get into abstracting the data logic we're going to create our base classes for the pie chart. Just like last time, we're going to need to create a new view controller for the graph. Let's call it 'STPieGraphViewController'.
Notice that we don't need to create a view this time because we will be able to use 'STGraphView'. Before we start setting things up let's jump into STStudentListViewController.h and import STPieGraphViewController.h. We also need to conform to the protocol STPieGraphViewControllerDelegate (which we will create later):
@interface STStudentListViewController : UIViewController <UITableViewDelegate, UITableViewDataSource, AddStudentViewControllerDelegate, UIActionSheetDelegate, STLineGraphViewControllerDelegate, STBarGraphViewControllerDelegate, STPieGraphViewControllerDelegate>
Switch over to the .m file. We need to add a button to the action sheet. Locate the graphButtonWasSelected: method. We're going to edit the second button text and add a third one:
- (void)graphButtonWasSelected:(id)sender { UIActionSheet *graphSelectionActionSheet = [[[UIActionSheet alloc] initWithTitle:@"Choose a graph" delegate:self cancelButtonTitle:@"Cancel" destructiveButtonTitle:nil otherButtonTitles:@"Enrolment over time", @"Subject totals - Bar", @"Subject totals - Pie", nil] autorelease]; [graphSelectionActionSheet showInView:[[UIApplication sharedApplication] keyWindow]]; }
Now jump into the actionSheet:clickedButtonAtIndex: method and add a clause for buttonIndex == 2:
else if (buttonIndex == 2) { STPieGraphViewController *graphVC = [[STPieGraphViewController alloc] init]; [graphVC setModalTransitionStyle:UIModalTransitionStyleFlipHorizontal]; [graphVC setModalPresentationStyle:UIModalPresentationFullScreen]; [graphVC setDelegate:self]; [graphVC setManagedObjectContext:[self managedObjectContext]]; [self presentModalViewController:graphVC animated:YES]; [graphVC release]; }
Just like last time, it will show some warnings because STPieGraphViewController doesn't have a delegate or managedObjectContext property.
So jump into STPieGraphViewController.h. Import 'CorePlot-CocoaTouch.h' and add the following properties and protocol declarations:
@protocol STPieGraphViewControllerDelegate @required - (void)doneButtonWasTapped:(id)sender; @end @interface STPieGraphViewController : UIViewController <CPTPieChartDelegate> @property (nonatomic, strong) CPTGraph *graph; @property (nonatomic, assign) id<STPieGraphViewControllerDelegate> delegate; @property (nonatomic, strong) NSManagedObjectContext *managedObjectContext; @end
It's important to point out that we aren't complying with CPTPieChartDataSource this time. This is because we will be abstracting the graph data logic from STBarGraphViewController into a separate dataSource class. Before we do that though, let's finish setting everything up. In the .m file, import 'STGraphView.h', synthesize the properties, and implement the dealloc method.
Finally, set up the loadView and viewDidLoad methods like below:
- (void)loadView { [super loadView]; [self setTitle:@"Enrolment by subject"]; [self setView:[[[STGraphView alloc] initWithFrame:self.view.frame] autorelease]]; CPTTheme *defaultTheme = [CPTTheme themeNamed:kCPTPlainWhiteTheme]; [self setGraph:(CPTGraph *)[defaultTheme newGraph]]; } - (void)viewDidLoad { [super viewDidLoad]; STGraphView *graphView = (STGraphView *)[self view]; [[graphView chartHostingView] setHostedGraph:[self graph]]; //Allow user to go back UINavigationItem *navigationItem = [[[UINavigationItem alloc] initWithTitle:self.title] autorelease]; [navigationItem setHidesBackButton:YES]; UINavigationBar *navigationBar = [[[UINavigationBar alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 44.0f)] autorelease]; [navigationBar pushNavigationItem:navigationItem animated:NO]; [self.view addSubview:navigationBar]; [navigationItem setRightBarButtonItem:[[[UIBarButtonItem alloc] initWithTitle:@"Done" style:UIBarButtonItemStyleDone target:[self delegate] action:@selector(doneButtonWasTapped:)] autorelease] animated:NO]; }
The above should be familiar by now. So let's look at abstracting the graph logic into a seperate class.
Step 2: Separating the Graph Logic
We've already written the logic for getting data for the total number of students in all the subjects; we don't want to have to write it again, luckily we don't have to. All the data source protocols for the plots inherit from 'CPTPlotDataSource', and it is this protocol that contains numberOfRecordsForPlot: and numberForPlot:fieldEnum:recordIndex methods.
Create a class called 'STAbstractSubjectDataSource.h' (inheriting from NSObject) in a new group called 'DataSource' in the graphing group. For the header file import 'CorePlot-CocoaTouch.h' and put the following properties and method declarations:
@interface STAbstractSubjectEnrollementDataSource : NSObject <CPTPlotDataSource> @property (nonatomic, strong) NSManagedObjectContext *managedObjectContext; - (id)initWithManagedObjectContext:(NSManagedObjectContext *)aManagedObjectContext; - (float)getTotalSubjects; - (float)getMaxEnrolled; - (NSArray *)getSubjectTitlesAsArray; @end
We subscribe to the 'CPTPlotDataSource' protocol. We create a custom init method that passes through a managedObjectContext so the object can access the data store. Finally, there are three helper methods that can help out with getting information about the subjects and enrollment. These are the same methods that currently exist in STBarGraphViewController. We're going to move those out and into the data source method.
Aside from the init method, the .m file doesn't contain any new code that you haven't seen before. It's just a matter of moving all the existing code from STBarGraphViewController into the dataSource object. The methods you should move are:
- (float)getTotalSubjects
- (float)getMaxEnrolled
- (NSArray *)getSubjectTitlesAsArray
- (NSUInteger)numberOfRecordsForPlot:(CPTPlot *)plot
- (NSNumber *)numberForPlot:(CPTPlot *)plot field:(NSUInteger)fieldEnum recordIndex:(NSUInteger)index
Also make sure you add in the custom init method:
- (id)initWithManagedObjectContext:(NSManagedObjectContext *)aManagedObjectContext { self = [super init]; if (self) { [self setManagedObjectContext:aManagedObjectContext]; } return self; }
Now we have a data source object that can provide the base data for both the pie and the bar chart. The abstract data source doesn't give us everything we need however, the barFillForBarPlot:recordIndex can't be implemented because it is part of CPTBarPlotDataSource. We're going to have to extend our abstract class to something specific for bar plots.
Create a new object in the Data Source group called 'STBarGraphSubjectEnrollementDataSource' that extends our abstract class. In the header subscribe to 'CPTBarPlotDataSource:
- (id)initWithManagedObjectContext:(NSManagedObjectContext *)aManagedObjectContext @interface STBarGraphSubjectEnrollementDataSource : STAbstractSubjectEnrollementDataSource <CPTBarPlotDataSource>
And in the .m file, implement the barFillForBarPlot method:
#pragma mark - CPTBarPlotDataSource Methods -(CPTFill *)barFillForBarPlot:(CPTBarPlot *)barPlot recordIndex:(NSUInteger)index { CPTColor *areaColor = nil; switch (index) { case 0: areaColor = [CPTColor redColor]; break; case 1: areaColor = [CPTColor blueColor]; break; case 2: areaColor = [CPTColor orangeColor]; break; case 3: areaColor = [CPTColor greenColor]; break; default: areaColor = [CPTColor purpleColor]; break; } CPTFill *barFill = [CPTFill fillWithColor:areaColor]; return barFill; }
Now go back to your STBarGraphViewControllers header file and import the new bar graph data source. You can now remove the 'CPTBarPlotDataSource' subscription. Jump into the .m file and delete all methods except loadView, viewDidLoad, and dealloc. We don't need them anymore.
We need to maintain a pointer to the data source and then release the pointer when the view is done with it. In the private interface, declare the property and then synthesize:
@interface STBarGraphViewController () @property (nonatomic, retain) STBarGraphSubjectEnrollementDataSource *barGraphDataSource; @end @implementation STBarGraphViewController @synthesize delegate; @synthesize managedObjectContext; @synthesize graph; @synthesize barGraphDataSource;
Make sure that you release it in the dealloc method as well. Create a new instance and set it as our property in the loadView method:
[self setBarGraphDataSource:[[[STBarGraphSubjectEnrollementDataSource alloc] initWithManagedObjectContext:[self managedObjectContext]] autorelease]];
Now in the viewDidLoad method we need to use our data source's helper methods for calculating the plot space and the custom labels:
CPTXYPlotSpace *studentPlotSpace = (CPTXYPlotSpace *)[graph defaultPlotSpace]; [studentPlotSpace setXRange:[CPTPlotRange plotRangeWithLocation:CPTDecimalFromInt(0) length:CPTDecimalFromInt([[self barGraphDataSource] getTotalSubjects] + 1)]]; [studentPlotSpace setYRange:[CPTPlotRange plotRangeWithLocation:CPTDecimalFromInt(0) length:CPTDecimalFromInt([[self barGraphDataSource] getMaxEnrolled] + 1)]];
NSArray *subjectsArray = [[self barGraphDataSource] getSubjectTitlesAsArray];
Save, build, and run. Everything should work just as before. If it does, we can get started on creating our pie graph!
Step 3: Creating the Pie Graph
We'll want to create a specific data source for the pie chart just as we did for the bar chart in case we need to implement any pie chart specific data source methods. Create a class called 'STPieGraphSubjectEnrollementDataSource' that inherits from 'STAbstractSubjectEnrollementDataSource'. In the header file, subscribe to the 'CPTPieChartDataSource' protocol. We won't implement any specific pie chart data source methods yet, we'll come back to that later.
Now that we have data sources, creating the pie graph is simple! Jump into the STBPieGraphViewController.m and import the pie graph data source object. Declare it as a property in the .m file, like we did last time:
@interface STPieGraphViewController () @property (nonatomic, strong) STPieGraphSubjectEnrollementDataSource *pieChartDataSource; @end @implementation STPieGraphViewController @synthesize managedObjectContext; @synthesize delegate; @synthesize graph; @synthesize pieChartDataSource;
Now create and set it in the loadView:
[self setPieChartDataSource:[[[STPieGraphSubjectEnrollementDataSource alloc] initWithManagedObjectContext:[self managedObjectContext]] autorelease]];
Finally, in the viewDidLoad method we need to create our pie chart, add it to our GraphView and remove the standard axis:
STGraphView *graphView = (STGraphView *)[self view]; [[graphView chartHostingView] setHostedGraph:[self graph]]; CPTPieChart *pieChart = [[CPTPieChart alloc] initWithFrame:[graph bounds]]; [pieChart setPieRadius:100.00]; [pieChart setIdentifier:@"Subject"]; [pieChart setStartAngle:M_PI_4]; [pieChart setSliceDirection:CPTPieDirectionCounterClockwise]; [pieChart setDataSource:pieChartDataSource]; [graph addPlot:pieChart]; [graph setAxisSet:nil]; [graph setBorderLineStyle:nil];
Most of the above should look familiar. Notice that it's not explicitly called a 'plot' because it doesn't rely on an x-axis or y-axis, but we still treat it much the same. There are some pie chart specific things we do here as well. We create a pie radius and starting angle. We also set a slice direction. Finally we set the 'axisSet' of the graph to nil so that we don't get the x and y lines.
And that should be all. Build and run to see your pie chart.
This is good, but it could do with some sort of indication as to what each color represents. A good way to do this is to use legends. To do this we create a 'CPTLegend' object that we add to our graph and implement a delegate method that returns the relevant title for the legend.
Let's create the CPTLegend object first. In our viewDidLoad method enter the following code underneath where we create our pie chart:
CPTLegend *theLegend = [CPTLegend legendWithGraph:[self graph]]; [theLegend setNumberOfColumns:2]; [[self graph] setLegend:theLegend]; [[self graph] setLegendAnchor:CPTRectAnchorBottom]; [[self graph] setLegendDisplacement:CGPointMake(0.0, 30.0)];
This creates a legend and adds it to our graph object. The number of columns determines how it will lay out the legend titles. We then set some attributes on the graph object that determines where the legend will be placed (the bottom) and some displacement to ensure it fully shows in the view.
We still need to provide the legend with titles though. There is a method specific to CPTPieChartDataSource that allows us to do this. Jump into the pie chart data source and implement the following code:
#pragma mark - CPTPieChartDataSource -(NSString *)legendTitleForPieChart:(CPTPieChart *)pieChart recordIndex:(NSUInteger)index { NSError *error = nil; NSFetchRequest *request = [[NSFetchRequest alloc] init]; NSEntityDescription *entity = [NSEntityDescription entityForName:@"STSubject" inManagedObjectContext:[self managedObjectContext]]; NSPredicate *predicate = [NSPredicate predicateWithFormat:@"subjectID == %d", index]; [request setEntity:entity]; [request setResultType:NSDictionaryResultType]; [request setPredicate:predicate]; [request setReturnsDistinctResults:NO]; [request setPropertiesToFetch:[NSArray arrayWithObject:@"subjectName"]]; NSArray *titleStrings = [[self managedObjectContext] executeFetchRequest:request error:&error]; NSDictionary *fetchedSubject = [titleStrings objectAtIndex:0]; return [fetchedSubject objectForKey:@"subjectName"]; }
This method simply gets the index of the legend and gets the title from the data store as a string and returns it.
Build and run and you should have an informative pie graph!
Wrap up
We've covered how to abstract the data logic from the controller into a separate object that's easier to manage and extend. We've also covered the basics of creating a pie chart.
That brings us to the end of the series. I hope you've found these tutorials helpful. There is much more that CorePlot can do, but this should give you a solid foundation to build upon. Good luck adding graphs to your own projects!
Comments