This is a blog post by iOS Tutorial Team member Steve Baranski, the founder of komorka technology, a provider of iOS development and consulting services.
Welcome to the second part of the Core Plot tutorial series!
In the first part of this series, you created a tab bar controller application that provided tabs for three separate charts/graphs. You added the Core Plot framework to the project and explored some of the key framework components as you built a dynamically-themed pie chart.
We’re going to start where we left off last time, so download theproject from last time if you don’t have it already.
In this second and final part of the series, you’ll expand the app by adding both a bar chart and a scatter plot. You’ll also learn ways to make your charts interactive by using other UIKit components and gestures.
What are you waiting for? Let’s get back to it!
Raising the Bar(Graph)
Your bar graph is going to display your portfolio’s individual closing prices for the last week of April 2012. In order to do that, you have to first set up the bar graph view.
Open the project from Part 1 of the tutorial and switch to MainStoryboard.storyboard. Select the view for the bar graph scene. In the Attributes Inspector, set the background color of the view to black.
To make the chart truly interactive, you’re going to add some switches and corresponding labels to the view. Drag three UISwitches – one for each stock – from the Object Library to the top of the Bar Graph scene. Then select each switch and set its coordinates to the following using the Size Inspector (the penultimate tab on the top half of the right sidebar):
- x = 76, y = 10
- x = 229, y = 10
- x = 381, y = 10
Now drag a UILabel next to each of the switches. The coordinates for each label are as follows:
- x = 20, y = 13
- x = 173, y = 13
- x = 325, y = 13
Set the Text for the labels to AAPL, GOOG, and MSFT using the Attributes Inspector for each label.
Finally, add another UIView as a subview. It should be positioned at x = 0, y = 45, w = 480, h = 206. You also need to declare its Class as CPTGraphHostingView in the Identity inspector. A screenshot of the completed interface is shown below:
Now switch to CPDBarGraphViewController.h and add the following to the end of the @interface line:
<CPTBarPlotDataSource, CPTBarPlotDelegate> |
The above declares the CPDBarGraphViewController as the delegate for the bar chart you’ll be creating. This requires a couple of delegate methods to be defined in the implementation file. So switch toCPDBarGraphViewController.m and add the following method stubs to the end of the file (but before the final @end):
#pragma mark - CPTPlotDataSource methods -(NSUInteger)numberOfRecordsForPlot:(CPTPlot *)plot { return 0; } -(NSNumber *)numberForPlot:(CPTPlot *)plot field:(NSUInteger)fieldEnum recordIndex:(NSUInteger)index { return [NSDecimalNumber numberWithUnsignedInteger:index]; } #pragma mark - CPTBarPlotDelegate methods -(void)barPlot:(CPTBarPlot *)plot barWasSelectedAtRecordIndex:(NSUInteger)index { } |
Try building and running the app and then switching to the Bar Graph tab. You should see something like the following:
Now it’s time to connect the interface to the view controller. To do so, you’ll add an IBOutlet for the CPTGraphHostingView and create IBActions associated with each of the switches. While you’re at it, you’ll also add the definitions for a few methods that you’ll need later on – and at least some are old friends from the first tutorial.
Add the following lines to the class continuation category (the section between the @interface and @end lines just below the #import) in CPDBarGraphViewController.m:
@property (nonatomic, strong) IBOutlet CPTGraphHostingView *hostView; @property (nonatomic, strong) CPTBarPlot *aaplPlot; @property (nonatomic, strong) CPTBarPlot *googPlot; @property (nonatomic, strong) CPTBarPlot *msftPlot; @property (nonatomic, strong) CPTPlotSpaceAnnotation *priceAnnotation; -(IBAction)aaplSwitched:(id)sender; -(IBAction)googSwitched:(id)sender; -(IBAction)msftSwitched:(id)sender; -(void)initPlot; -(void)configureGraph; -(void)configurePlots; -(void)configureAxes; -(void)hideAnnotation:(CPTGraph *)graph; |
In addition to the IBOutlet and three IBActions, you retain references to each of your plots, a plot space annotation that will present the price of a tapped bar, and a few private methods that you’ll implement soon.
Of course, now that you’ve added properties, you need to synthesize them. Add the following right after the @implementation line:
CGFloat const CPDBarWidth = 0.25f; CGFloat const CPDBarInitialX = 0.25f; @synthesize hostView = hostView_; @synthesize aaplPlot = aaplPlot_; @synthesize googPlot = googPlot_; @synthesize msftPlot = msftPlot_; @synthesize priceAnnotation = priceAnnotation_; |
In addition to synthesizing the properties you added previously, the code also declares a couple of constant values that will be used later on.
Add the following right below the @synthesize lines you added just now:
#pragma mark - UIViewController lifecycle methods -(void)viewDidLoad { [super viewDidLoad]; [self initPlot]; } #pragma mark - IBActions -(IBAction)aaplSwitched:(id)sender { } -(IBAction)googSwitched:(id)sender { } -(IBAction)msftSwitched:(id)sender { } #pragma mark - Chart behavior -(void)initPlot { self.hostView.allowPinchScaling = NO; [self configureGraph]; [self configurePlots]; [self configureAxes]; } -(void)configureGraph { } -(void)configurePlots { } -(void)configureAxes { } -(void)hideAnnotation:(CPTGraph *)graph { } |
This is what the above code does:
- Initializes the plot via viewDidLoad. As in Part 1, initPlot serves as an outline for the steps you’ll take to construct the chart.
- Uses each of the “configure” methods in initPlot to construct the chart for display.
- Calls hideAnnotation: in response to the user tapping on the switches at the top of the screen.
Time to wire up your outlets and actions! Do the following:
- Return to MainStoryboard.storyboard.
- Select the Bar Graph View Controller in the Document Outline.
- Switch to the Connections tab in Utilities (right sidebar).
- Connect the hostView outlet to the subview that you declared to be of type CPTGraphHostingView.
- Connect aaplSwitched:, googSwitched:, and msftSwitched: actions, in that order, to the “Value Changed” event of the switches at the top of the screen.
The final result should be similar to what’s shown in the screenshot below:
Graphing It Up!
Now that the UI is all hooked up, it’s time to configure the graph. Add the following lines toconfigureGraph in CPDBarGraphViewController.m:
// 1 - Create the graph CPTGraph *graph = [[CPTXYGraph alloc] initWithFrame:self.hostView.bounds]; graph.plotAreaFrame.masksToBorder = NO; self.hostView.hostedGraph = graph; // 2 - Configure the graph [graph applyTheme:[CPTTheme themeNamed:kCPTPlainBlackTheme]]; graph.paddingBottom = 30.0f; graph.paddingLeft = 30.0f; graph.paddingTop = -1.0f; graph.paddingRight = -5.0f; // 3 - Set up styles CPTMutableTextStyle *titleStyle = [CPTMutableTextStyle textStyle]; titleStyle.color = [CPTColor whiteColor]; titleStyle.fontName = @"Helvetica-Bold"; titleStyle.fontSize = 16.0f; // 4 - Set up title NSString *title = @"Portfolio Prices: April 23 - 27, 2012"; graph.title = title; graph.titleTextStyle = titleStyle; graph.titlePlotAreaFrameAnchor = CPTRectAnchorTop; graph.titleDisplacement = CGPointMake(0.0f, -16.0f); // 5 - Set up plot space CGFloat xMin = 0.0f; CGFloat xMax = [[[CPDStockPriceStore sharedInstance] datesInWeek] count]; CGFloat yMin = 0.0f; CGFloat yMax = 800.0f; // should determine dynamically based on max price CPTXYPlotSpace *plotSpace = (CPTXYPlotSpace *) graph.defaultPlotSpace; plotSpace.xRange = [CPTPlotRange plotRangeWithLocation:CPTDecimalFromFloat(xMin) length:CPTDecimalFromFloat(xMax)]; plotSpace.yRange = [CPTPlotRange plotRangeWithLocation:CPTDecimalFromFloat(yMin) length:CPTDecimalFromFloat(yMax)]; |
Let’s break down the above code section-by-section:
- First, instantiate a CPTXYGraph. In this case, you designate it the hosted graph of the host view.
- Then, declare the default theme to be “plain black,” and offset the padding on the left and bottom of the bar chart to accommodate the anticipated axes.
- Define a CPTMutableTextStyle to be used for the title of the chart.
- Create a title for the graph, set the previously defined style to the title, and set the title position.
- Configure the CPTXYPlotSpace. The plot space is responsible for mapping device coordinates to the coordinates of your graph. In this instance, you’re plotting three stocks that have the same plot space – U.S. dollars. However, it’s entirely possible to have a separate plot space for each plot on a graph.
In this particular instance, you use existing knowledge of the stock price range to set the plot space. Later on in the tutorial, you’ll learn how to auto-size the plot space when you don’t know the plot range at the outset.
Now that you have your graph, it’s time to add some plots to it! Add the following code to configurePlots:
// 1 - Set up the three plots self.aaplPlot = [CPTBarPlot tubularBarPlotWithColor:[CPTColor redColor] horizontalBars:NO]; self.aaplPlot.identifier = CPDTickerSymbolAAPL; self.googPlot = [CPTBarPlot tubularBarPlotWithColor:[CPTColor greenColor] horizontalBars:NO]; self.googPlot.identifier = CPDTickerSymbolGOOG; self.msftPlot = [CPTBarPlot tubularBarPlotWithColor:[CPTColor blueColor] horizontalBars:NO]; self.msftPlot.identifier = CPDTickerSymbolMSFT; // 2 - Set up line style CPTMutableLineStyle *barLineStyle = [[CPTMutableLineStyle alloc] init]; barLineStyle.lineColor = [CPTColor lightGrayColor]; barLineStyle.lineWidth = 0.5; // 3 - Add plots to graph CPTGraph *graph = self.hostView.hostedGraph; CGFloat barX = CPDBarInitialX; NSArray *plots = [NSArray arrayWithObjects:self.aaplPlot, self.googPlot, self.msftPlot, nil]; for (CPTBarPlot *plot in plots) { plot.dataSource = self; plot.delegate = self; plot.barWidth = CPTDecimalFromDouble(CPDBarWidth); plot.barOffset = CPTDecimalFromDouble(barX); plot.lineStyle = barLineStyle; [graph addPlot:plot toPlotSpace:graph.defaultPlotSpace]; barX += CPDBarWidth; } |
Here’s what the above code does:
- You instantiate each bar plot by passing NO for horizontalBars, since you want the bars in your plot to be vertical. You use the stock ticker symbol as the identifier – this will be crucial for the delegate method implementation (which you’ll get to next).
- You instantiate a CPTMutableLineStyle instance that represents the outer border of each bar plot.
- You apply a “common configuration” to each bar plot. This configuration includes setting the data source and delegate, setting the width and relative (left-right) placement of each bar in the plot, setting the line style, and finally, adding the plot to the graph.
While you still won’t see the actual bar graph in action, it might be a good idea at this point to compile your app to make sure that everything works.
In order to actually display the data on the bar graph, you need to implement the delegate methods that provide the necessary data to the graph. This is where the skeleton code you added earlier comes in.
Replace the placeholder code in numberOfRecordsForPlot: with the following line:
return [[[CPDStockPriceStore sharedInstance] datesInWeek] count]; |
The above queries the CPDStockPriceStore class to get the number of records that the graph will be displaying.
Replace the skeleton code in numberForPlot:field:recordIndex: with the following:
if ((fieldEnum == CPTBarPlotFieldBarTip) && (index < [[[CPDStockPriceStore sharedInstance] datesInWeek] count])) { if ([plot.identifier isEqual:CPDTickerSymbolAAPL]) { return [[[CPDStockPriceStore sharedInstance] weeklyPrices:CPDTickerSymbolAAPL] objectAtIndex:index]; } else if ([plot.identifier isEqual:CPDTickerSymbolGOOG]) { return [[[CPDStockPriceStore sharedInstance] weeklyPrices:CPDTickerSymbolGOOG] objectAtIndex:index]; } else if ([plot.identifier isEqual:CPDTickerSymbolMSFT]) { return [[[CPDStockPriceStore sharedInstance] weeklyPrices:CPDTickerSymbolMSFT] objectAtIndex:index]; } } return [NSDecimalNumber numberWithUnsignedInteger:index]; |
The CPTBarPlotFieldBarTip value in the above code indicates the relative size of the bar chart. You use the identifier set in configurePlots to figure out the stock price for which you need to retrieve the data. The recordIndex corresponds to the index of the price of interest.
Compile and run. You should see something similar to the following:
You’re getting there! But notice there’s no indication of what each axis represents. Plus, what are those white lines on the left edge of the graph? You’ll fix both of these issues next.
Add the following to configureAxes:
// 1 - Configure styles CPTMutableTextStyle *axisTitleStyle = [CPTMutableTextStyle textStyle]; axisTitleStyle.color = [CPTColor whiteColor]; axisTitleStyle.fontName = @"Helvetica-Bold"; axisTitleStyle.fontSize = 12.0f; CPTMutableLineStyle *axisLineStyle = [CPTMutableLineStyle lineStyle]; axisLineStyle.lineWidth = 2.0f; axisLineStyle.lineColor = [[CPTColor whiteColor] colorWithAlphaComponent:1]; // 2 - Get the graph's axis set CPTXYAxisSet *axisSet = (CPTXYAxisSet *) self.hostView.hostedGraph.axisSet; // 3 - Configure the x-axis axisSet.xAxis.labelingPolicy = CPTAxisLabelingPolicyNone; axisSet.xAxis.title = @"Days of Week (Mon - Fri)"; axisSet.xAxis.titleTextStyle = axisTitleStyle; axisSet.xAxis.titleOffset = 10.0f; axisSet.xAxis.axisLineStyle = axisLineStyle; // 4 - Configure the y-axis axisSet.yAxis.labelingPolicy = CPTAxisLabelingPolicyNone; axisSet.yAxis.title = @"Price"; axisSet.yAxis.titleTextStyle = axisTitleStyle; axisSet.yAxis.titleOffset = 5.0f; axisSet.yAxis.axisLineStyle = axisLineStyle; |
Briefly, the above code first defines styles for the axis lines and titles. Then, the code retrieves the axis set for the graph and configures the settings for the x and y axes.
Build and run to see the results of the above changes.
Grinding Axes
Much better, right? The only drawback is that your axes are plain – giving no idea of the exact stock price.
You can fix this so that when a user taps on an individual bar chart, the app will display the price that the bar represents. To do this, add the following code to barPlot:barWasSelectedAtRecordIndex::
// 1 - Is the plot hidden? if (plot.isHidden == YES) { return; } // 2 - Create style, if necessary static CPTMutableTextStyle *style = nil; if (!style) { style = [CPTMutableTextStyle textStyle]; style.color= [CPTColor yellowColor]; style.fontSize = 16.0f; style.fontName = @"Helvetica-Bold"; } // 3 - Create annotation, if necessary NSNumber *price = [self numberForPlot:plot field:CPTBarPlotFieldBarTip recordIndex:index]; if (!self.priceAnnotation) { NSNumber *x = [NSNumber numberWithInt:0]; NSNumber *y = [NSNumber numberWithInt:0]; NSArray *anchorPoint = [NSArray arrayWithObjects:x, y, nil]; self.priceAnnotation = [[CPTPlotSpaceAnnotation alloc] initWithPlotSpace:plot.plotSpace anchorPlotPoint:anchorPoint]; } // 4 - Create number formatter, if needed static NSNumberFormatter *formatter = nil; if (!formatter) { formatter = [[NSNumberFormatter alloc] init]; [formatter setMaximumFractionDigits:2]; } // 5 - Create text layer for annotation NSString *priceValue = [formatter stringFromNumber:price]; CPTTextLayer *textLayer = [[CPTTextLayer alloc] initWithText:priceValue style:style]; self.priceAnnotation.contentLayer = textLayer; // 6 - Get plot index based on identifier NSInteger plotIndex = 0; if ([plot.identifier isEqual:CPDTickerSymbolAAPL] == YES) { plotIndex = 0; } else if ([plot.identifier isEqual:CPDTickerSymbolGOOG] == YES) { plotIndex = 1; } else if ([plot.identifier isEqual:CPDTickerSymbolMSFT] == YES) { plotIndex = 2; } // 7 - Get the anchor point for annotation CGFloat x = index + CPDBarInitialX + (plotIndex * CPDBarWidth); NSNumber *anchorX = [NSNumber numberWithFloat:x]; CGFloat y = [price floatValue] + 40.0f; NSNumber *anchorY = [NSNumber numberWithFloat:y]; self.priceAnnotation.anchorPlotPoint = [NSArray arrayWithObjects:anchorX, anchorY, nil]; // 8 - Add the annotation [plot.graph.plotAreaFrame.plotArea addAnnotation:self.priceAnnotation]; |
This requires a bit of explanation:
- You don’t display an annotation for a hidden plot. While the plots currently don’t have the ability to be hidden, you’ll be implementing this in the next step when you integrate the switches with the bar chart.
- Create another text style – your annotations will be presented in bold yellow.
- Get the price for the specified plot and then create an annotation object if one doesn’t exist.
- Create a number formatter if one doesn’t exist, since you’ll need to format the price for display.
- Create a text layer using the formatted price, and set the content layer for the annotation to this new text layer.
- Get the plot index for the plot for which you’ll display the annotation.
- Calculate the annotation position based on the plot index, and then set the anchorPlotPoint for the annotation using the calculated position.
- Add the annotation to the graph.
Build and test the app. Every time you tap on a bar in your chart, the value for that bar should pop up right above the bar. Nifty! :]
Hide and Seek
The bar graph looks great and the information displays correctly. You can tap to see the value of each bar. But the switches at the top of the screen do nothing. It’s time to rectify that.
The basic idea is to allow the user to toggle the display of bar charts for a given stock using the switches. Replace the skeleton implementations for aaplSwitched:, googSwitched:, and msftSwitched: with the following:
-(IBAction)aaplSwitched:(id)sender { BOOL on = [((UISwitch *) sender) isOn]; if (!on) { [self hideAnnotation:self.aaplPlot.graph]; } [self.aaplPlot setHidden:!on]; } -(IBAction)googSwitched:(id)sender { BOOL on = [((UISwitch *) sender) isOn]; if (!on) { [self hideAnnotation:self.googPlot.graph]; } [self.googPlot setHidden:!on]; } -(IBAction)msftSwitched:(id)sender { BOOL on = [((UISwitch *) sender) isOn]; if (!on) { [self hideAnnotation:self.msftPlot.graph]; } [self.msftPlot setHidden:!on]; } |
The logic is fairly simple. If the switch is set to off, the corresponding plot and any visible annotation is hidden. If the switch is set to on, then the plot is made visible again.
Since the above methods execute hideAnnotation:, you need to add the implementation code for the method, for which you’ve already added a placeholder:
if ((graph.plotAreaFrame.plotArea) && (self.priceAnnotation)) { [graph.plotAreaFrame.plotArea removeAnnotation:self.priceAnnotation]; self.priceAnnotation = nil; } |
The code simply removes an annotation, if it exists.
Build and run. You can now toggle each bar chart to your heart’s content. Nice work!
The Scatter Plot
You used both Core Plot delegate methods and UIKit elements to interact with your bar chart. For the scatter plot, you’ll take advantage of Core Plot’s built-in support for pinch zooming to provide some additional interactivity. You’ll also make your graphs more attractive in other ways.
You’re going to create a scatter plot that will present the closing prices of each portfolio stock for each trading day in April 2012.
As before, you need to set up a Core Plot delegate – this time in CPDScatterPlotViewController.h (at the end of the @interface line):
<CPTPlotDataSource> |
Next, add the skeletons for the datasource delegate methods at the end ofCPDScatterPlotViewController.m (above the @end line):
#pragma mark - CPTPlotDataSource methods -(NSUInteger)numberOfRecordsForPlot:(CPTPlot *)plot { return 0; } -(NSNumber *)numberForPlot:(CPTPlot *)plot field:(NSUInteger)fieldEnum recordIndex:(NSUInteger)index { return [NSDecimalNumber zero]; } |
You also need to add a property for the hostView. Add the following in CPDScatterPlotViewController.h, just above the @end line:
@property (nonatomic, strong) CPTGraphHostingView *hostView; |
Do note that unlike with the Bar Graph, you don’t specify the hostView as an IBOutlet. That’s because this time you’ll create the hostView via code instead of setting it up in Interface Builder.
Of course, the next step is to synthesize the property at the top of CPDScatterPlotViewController.m, below the @implementation line:
@synthesize hostView = hostView_; |
Now add the following just below that @synthesize line:
#pragma mark - UIViewController lifecycle methods -(void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; [self initPlot]; } #pragma mark - Chart behavior -(void)initPlot { [self configureHost]; [self configureGraph]; [self configurePlots]; [self configureAxes]; } -(void)configureHost { } -(void)configureGraph { } -(void)configurePlots { } -(void)configureAxes { } |
By now these steps should seem routine. You start the initialization process in viewDidAppear:, creating your host view, configuring the graph, initializing your plot, and setting up the axes.
So let’s jump straight into the configureHost method. Add the following code to it:
self.hostView = [(CPTGraphHostingView *) [CPTGraphHostingView alloc] initWithFrame:self.view.bounds]; self.hostView.allowPinchScaling = YES; [self.view addSubview:self.hostView]; |
Part 1 of the tutorial mentioned that pinch zooming was enabled by default in CorePlot. So the second line of code above isn’t absolutely necessary. However, it’s there to make the fact abundantly clear. The rest should be fairly straightforward: it simply creates a graph hosting view and adds it to the main view.
Next, add the following code to configureGraph:
// 1 - Create the graph CPTGraph *graph = [[CPTXYGraph alloc] initWithFrame:self.hostView.bounds]; [graph applyTheme:[CPTTheme themeNamed:kCPTDarkGradientTheme]]; self.hostView.hostedGraph = graph; // 2 - Set graph title NSString *title = @"Portfolio Prices: April 2012"; graph.title = title; // 3 - Create and set text style CPTMutableTextStyle *titleStyle = [CPTMutableTextStyle textStyle]; titleStyle.color = [CPTColor whiteColor]; titleStyle.fontName = @"Helvetica-Bold"; titleStyle.fontSize = 16.0f; graph.titleTextStyle = titleStyle; graph.titlePlotAreaFrameAnchor = CPTRectAnchorTop; graph.titleDisplacement = CGPointMake(0.0f, 10.0f); // 4 - Set padding for plot area [graph.plotAreaFrame setPaddingLeft:30.0f]; [graph.plotAreaFrame setPaddingBottom:30.0f]; // 5 - Enable user interactions for plot space CPTXYPlotSpace *plotSpace = (CPTXYPlotSpace *) graph.defaultPlotSpace; plotSpace.allowsUserInteraction = YES; |
Most of the above should be also routine by now. But let’s go through it section-by-section for the sake of clarity:
- Create a graph, apply the “dark gradient” theme to it, and designate it as the hosted graph for your host view.
- Set the graph’s title.
- Customize the title’s appearance and placement.
- Set the plot area frame. If you think of your graph as a framed picture, the plot area frame is like the matte border around the picture. In this case, you’ve added padding to the left and right to accommodate the axis labels and titles for the graph.
- Finally, enable user interactions for the plot space. This is necessary for the pinch and zoom to work.
Then, add the following to configurePlots:
// 1 - Get graph and plot space CPTGraph *graph = self.hostView.hostedGraph; CPTXYPlotSpace *plotSpace = (CPTXYPlotSpace *) graph.defaultPlotSpace; // 2 - Create the three plots CPTScatterPlot *aaplPlot = [[CPTScatterPlot alloc] init]; aaplPlot.dataSource = self; aaplPlot.identifier = CPDTickerSymbolAAPL; CPTColor *aaplColor = [CPTColor redColor]; [graph addPlot:aaplPlot toPlotSpace:plotSpace]; CPTScatterPlot *googPlot = [[CPTScatterPlot alloc] init]; googPlot.dataSource = self; googPlot.identifier = CPDTickerSymbolGOOG; CPTColor *googColor = [CPTColor greenColor]; [graph addPlot:googPlot toPlotSpace:plotSpace]; CPTScatterPlot *msftPlot = [[CPTScatterPlot alloc] init]; msftPlot.dataSource = self; msftPlot.identifier = CPDTickerSymbolMSFT; CPTColor *msftColor = [CPTColor blueColor]; [graph addPlot:msftPlot toPlotSpace:plotSpace]; // 3 - Set up plot space [plotSpace scaleToFitPlots:[NSArray arrayWithObjects:aaplPlot, googPlot, msftPlot, nil]]; CPTMutablePlotRange *xRange = [plotSpace.xRange mutableCopy]; [xRange expandRangeByFactor:CPTDecimalFromCGFloat(1.1f)]; plotSpace.xRange = xRange; CPTMutablePlotRange *yRange = [plotSpace.yRange mutableCopy]; [yRange expandRangeByFactor:CPTDecimalFromCGFloat(1.2f)]; plotSpace.yRange = yRange; // 4 - Create styles and symbols CPTMutableLineStyle *aaplLineStyle = [aaplPlot.dataLineStyle mutableCopy]; aaplLineStyle.lineWidth = 2.5; aaplLineStyle.lineColor = aaplColor; aaplPlot.dataLineStyle = aaplLineStyle; CPTMutableLineStyle *aaplSymbolLineStyle = [CPTMutableLineStyle lineStyle]; aaplSymbolLineStyle.lineColor = aaplColor; CPTPlotSymbol *aaplSymbol = [CPTPlotSymbol ellipsePlotSymbol]; aaplSymbol.fill = [CPTFill fillWithColor:aaplColor]; aaplSymbol.lineStyle = aaplSymbolLineStyle; aaplSymbol.size = CGSizeMake(6.0f, 6.0f); aaplPlot.plotSymbol = aaplSymbol; CPTMutableLineStyle *googLineStyle = [googPlot.dataLineStyle mutableCopy]; googLineStyle.lineWidth = 1.0; googLineStyle.lineColor = googColor; googPlot.dataLineStyle = googLineStyle; CPTMutableLineStyle *googSymbolLineStyle = [CPTMutableLineStyle lineStyle]; googSymbolLineStyle.lineColor = googColor; CPTPlotSymbol *googSymbol = [CPTPlotSymbol starPlotSymbol]; googSymbol.fill = [CPTFill fillWithColor:googColor]; googSymbol.lineStyle = googSymbolLineStyle; googSymbol.size = CGSizeMake(6.0f, 6.0f); googPlot.plotSymbol = googSymbol; CPTMutableLineStyle *msftLineStyle = [msftPlot.dataLineStyle mutableCopy]; msftLineStyle.lineWidth = 2.0; msftLineStyle.lineColor = msftColor; msftPlot.dataLineStyle = msftLineStyle; CPTMutableLineStyle *msftSymbolLineStyle = [CPTMutableLineStyle lineStyle]; msftSymbolLineStyle.lineColor = msftColor; CPTPlotSymbol *msftSymbol = [CPTPlotSymbol diamondPlotSymbol]; msftSymbol.fill = [CPTFill fillWithColor:msftColor]; msftSymbol.lineStyle = msftSymbolLineStyle; msftSymbol.size = CGSizeMake(6.0f, 6.0f); msftPlot.plotSymbol = msftSymbol; |
While the above code is fairly long, it’s straightforward. It does the following:
- Get a reference to the graph and its default plot space for use in the code below.
- Create each plot, setting the data source, identifier, and color for each chart.
- Set the plot space scale to fit each of its three plots, then expand the range just a bit more to provide some space around the graph instead of having it fill the entire screen.
- Configure the style for each plot’s line. You also create and apply a style to the actual data points along each line. Core Plot offers a number of symbols you can use to differentiate the charts from each other.
Compile and build the app to make sure there are no errors. While you won’t see the plots yet (since you haven’t fleshed out the data source delegate methods), you should see something similar to the following:
And Data Makes Three
The basic chart is now there. All you need to do is add the data source delegate methods to provide data for the charts.
Replace the code in numberOfRecordsForPlot: with the following:
return [[[CPDStockPriceStore sharedInstance] datesInMonth] count]; |
The above simply returns the number of dates in the month based on the data source. Since the chart will have a plot point for each day of the month, this defines the number of records for each plot.
Now replace the contents of numberForPlot:field:recordIndex: with the following:
NSInteger valueCount = [[[CPDStockPriceStore sharedInstance] datesInMonth] count]; switch (fieldEnum) { case CPTScatterPlotFieldX: if (index < valueCount) { return [NSNumber numberWithUnsignedInteger:index]; } break; case CPTScatterPlotFieldY: if ([plot.identifier isEqual:CPDTickerSymbolAAPL] == YES) { return [[[CPDStockPriceStore sharedInstance] monthlyPrices:CPDTickerSymbolAAPL] objectAtIndex:index]; } else if ([plot.identifier isEqual:CPDTickerSymbolGOOG] == YES) { return [[[CPDStockPriceStore sharedInstance] monthlyPrices:CPDTickerSymbolGOOG] objectAtIndex:index]; } else if ([plot.identifier isEqual:CPDTickerSymbolMSFT] == YES) { return [[[CPDStockPriceStore sharedInstance] monthlyPrices:CPDTickerSymbolMSFT] objectAtIndex:index]; } break; } return [NSDecimalNumber zero]; |
This should look familiar, except for the different field enumeration values. In this case, you need to return a value for either the x-axis or the y-axis, the combination of which defines the coordinates on the chart for each plot point.
So, you return the index value for the CPTScatterPlotFieldX enumeration, since the x-axis is simply the day of the month. When CPTScatterPlotFieldY is received, you inspect the plot identifier to see which series of monthly price data to fetch price information from.
Build and run the app again. This time you should see the scatter plots in all their glory. :]
But wait! There are those same lines on the left edge as there were on the bar graph. Plus, the numbers along the x-axis are all garbled. But this time around, you know that this is because the axes have not been configured yet. So fix it!
Add the following to configureAxes:
// 1 - Create styles CPTMutableTextStyle *axisTitleStyle = [CPTMutableTextStyle textStyle]; axisTitleStyle.color = [CPTColor whiteColor]; axisTitleStyle.fontName = @"Helvetica-Bold"; axisTitleStyle.fontSize = 12.0f; CPTMutableLineStyle *axisLineStyle = [CPTMutableLineStyle lineStyle]; axisLineStyle.lineWidth = 2.0f; axisLineStyle.lineColor = [CPTColor whiteColor]; CPTMutableTextStyle *axisTextStyle = [[CPTMutableTextStyle alloc] init]; axisTextStyle.color = [CPTColor whiteColor]; axisTextStyle.fontName = @"Helvetica-Bold"; axisTextStyle.fontSize = 11.0f; CPTMutableLineStyle *tickLineStyle = [CPTMutableLineStyle lineStyle]; tickLineStyle.lineColor = [CPTColor whiteColor]; tickLineStyle.lineWidth = 2.0f; CPTMutableLineStyle *gridLineStyle = [CPTMutableLineStyle lineStyle]; tickLineStyle.lineColor = [CPTColor blackColor]; tickLineStyle.lineWidth = 1.0f; // 2 - Get axis set CPTXYAxisSet *axisSet = (CPTXYAxisSet *) self.hostView.hostedGraph.axisSet; // 3 - Configure x-axis CPTAxis *x = axisSet.xAxis; x.title = @"Day of Month"; x.titleTextStyle = axisTitleStyle; x.titleOffset = 15.0f; x.axisLineStyle = axisLineStyle; x.labelingPolicy = CPTAxisLabelingPolicyNone; x.labelTextStyle = axisTextStyle; x.majorTickLineStyle = axisLineStyle; x.majorTickLength = 4.0f; x.tickDirection = CPTSignNegative; CGFloat dateCount = [[[CPDStockPriceStore sharedInstance] datesInMonth] count]; NSMutableSet *xLabels = [NSMutableSet setWithCapacity:dateCount]; NSMutableSet *xLocations = [NSMutableSet setWithCapacity:dateCount]; NSInteger i = 0; for (NSString *date in [[CPDStockPriceStore sharedInstance] datesInMonth]) { CPTAxisLabel *label = [[CPTAxisLabel alloc] initWithText:date textStyle:x.labelTextStyle]; CGFloat location = i++; label.tickLocation = CPTDecimalFromCGFloat(location); label.offset = x.majorTickLength; if (label) { [xLabels addObject:label]; [xLocations addObject:[NSNumber numberWithFloat:location]]; } } x.axisLabels = xLabels; x.majorTickLocations = xLocations; // 4 - Configure y-axis CPTAxis *y = axisSet.yAxis; y.title = @"Price"; y.titleTextStyle = axisTitleStyle; y.titleOffset = -40.0f; y.axisLineStyle = axisLineStyle; y.majorGridLineStyle = gridLineStyle; y.labelingPolicy = CPTAxisLabelingPolicyNone; y.labelTextStyle = axisTextStyle; y.labelOffset = 16.0f; y.majorTickLineStyle = axisLineStyle; y.majorTickLength = 4.0f; y.minorTickLength = 2.0f; y.tickDirection = CPTSignPositive; NSInteger majorIncrement = 100; NSInteger minorIncrement = 50; CGFloat yMax = 700.0f; // should determine dynamically based on max price NSMutableSet *yLabels = [NSMutableSet set]; NSMutableSet *yMajorLocations = [NSMutableSet set]; NSMutableSet *yMinorLocations = [NSMutableSet set]; for (NSInteger j = minorIncrement; j <= yMax; j += minorIncrement) { NSUInteger mod = j % majorIncrement; if (mod == 0) { CPTAxisLabel *label = [[CPTAxisLabel alloc] initWithText:[NSString stringWithFormat:@"%i", j] textStyle:y.labelTextStyle]; NSDecimal location = CPTDecimalFromInteger(j); label.tickLocation = location; label.offset = -y.majorTickLength - y.labelOffset; if (label) { [yLabels addObject:label]; } [yMajorLocations addObject:[NSDecimalNumber decimalNumberWithDecimal:location]]; } else { [yMinorLocations addObject:[NSDecimalNumber decimalNumberWithDecimal:CPTDecimalFromInteger(j)]]; } } y.axisLabels = yLabels; y.majorTickLocations = yMajorLocations; y.minorTickLocations = yMinorLocations; |
Here’s what the above code does:
- Creates line styles for the axis title, axis lines, axis labels, tick lines, and grid lines. In this case, your grid lines will be horizontal at a defined “major” increment.
- Gets the axis set for the graph. The axis set contains information about the graph’s axes.
- Configures the x-axis. The important thing here is that when using a labeling policy of CPTAxisLabelingPolicyNone, you have to declare the locations and values of each tick mark and tick label. These are assembled in the loop at the end of this section.
- Configures the y-axis. The code is pretty similar to step #3, except in this case, you create axis labels at each $100 price increment.
Build and run to see the results of your hard work!
That’s fantastic! You can now pan around or pinch-zoom your plot to inspect the chart in more detail. Now you’re plotting in style!
Where to Go From Here?
Here is an example project with all of the code from the tutorial series so far.
Whew – that was fun! Hopefully this two-part tutorial underscores the power of Core Plot and gives you some ideas about how to use Core Plot in your own applications. Make sure to refer to the project site for more information, including documentation, examples, and tips.
Also, if you have any questions or comments, please join the forum discussion below.
Happy plotting!