Avatar of marc

by

Visualizing Data with Core-Plot

January 26, 2010 in Cocoa, Data Abstract, iOS, Mac, Xcode

Core Plot is an open source graphing framework for Cocoa developers that makes it really easy to add graphs to your applications for the Mac and iPhone. Over the past half year, i’ve had the chance to use Core Plot in several of the internal applications i have been working on and have been very impressed with it, so i wanted to talk a little bit about how to use the library, in general, and how to make generate charts based on data in a Data Abstract for OS X application, in particular.

The example shown here is from a small iPhone application i wrote to keep track of the status of our Continuous Integration servers. The application shows the success (or errors) of any builds done on the machine, as well as – and this is the part where Core Plot comes in – graphs the number of tests that run and fail, over time:

In the application in question, the chart is being displayed in a cell within a UITableView, so our example starts out by implementing a custom UITableViewCell class that will host the graph. But the same general principles apply to show a chart anywhere else (and, replacing UIView with NSView, for using charts in a desktop app).

Core Plot uses a Core Animation as underlying technology, and as such draws itself in a specialized view class CPLayerHostingView. So we start out by crating this view, and adding it as sub-view:

- (id)initWithStyle:(UITableViewCellStyle)style 
{
  self = [super initWithStyle:style reuseIdentifier:@"MyChartCell"];
  if (self)
  {
    CGRect frame = self.contentView.bounds;
    CPLayerHostingView *chartView = [[CPLayerHostingView alloc] initWithFrame: frame];
    [self addSubview:chartView];
    //...

Once the CPLayerHostingView is created, we can add a graphs to it, and configure them. There are several graph types supported, but we want a regular line graph with X/Y coordinates, so we’ll choose a CPXYGraph class. We’ll add the graph to the view, and set a padding, to give us some room between the edges of the table cell:

  // create an CPXYGraph and host it inside the view
  CPTheme *theme = [CPTheme themeNamed:kCPPlainWhiteTheme];
  graph = (CPXYGraph *)[theme newGraph];	
  chartView.hostedLayer = graph;
 
  graph.paddingLeft = 10.0;
  graph.paddingTop = 10.0;
  graph.paddingRight = 10.0;
  graph.paddingBottom = 10.0;

The next step is to set up a plot space. A plot space defines the coordinate system for one or more charts in a graph, essentially mapping logical values to the area in the graph. For example, your chart might show values ranging from, say, 0 to 1000. The plot space defines the scale of those values in relation to the graph. It also defines the visual range of values that can be seen on screen.

To match our X/Y graph, we’ll create a CPXYPlotSpace, and set it to a range of 0-100 for both axis:

  CPXYPlotSpace *plotSpace = (CPXYPlotSpace *)graph.defaultPlotSpace;
  plotSpace.xRange = [CPPlotRange plotRangeWithLocation:CPDecimalFromFloat(0)
                                                 length:CPDecimalFromFloat(100)];
  plotSpace.yRange = [CPPlotRange plotRangeWithLocation:CPDecimalFromFloat(0)
                                                 length:CPDecimalFromFloat(100)];

Finally, we’ll set up to axis, for the X and Y coordinates. Axis provide labels and tick-marks for the viewer, to give context to the values being shown.

The X/Y graph already provides a set of axis in for of a – you guessed it – CPXYAxisSet class. There are several properties on the axis’ that are worth looking at at tweaking to to suite out needs:

  • majorIntervalLength defines the number of units between “big” ticks on the axis. In this case it’s set to show one very 10 units. (if it were not for the exclusionRanges, discussed below, a numeric label would show for each major tick, as well).
  • minorTicksPerInterval specified how many small ticks will be shown between each major one. in this case, a value of 2 indicated that small would show for 5, 15, 25, etc. (a value of 9 would show ticks for every single unit).
  • Finally, exclusionRanges allows to define areas where no axis labels will be drawn. In my app, i don’t want to see any labels, so i set a range to exclude the entire visible graph
  CPXYAxisSet *axisSet = (CPXYAxisSet *)graph.axisSet;
 
  CPXYAxis *x = axisSet.xAxis;
  x.majorIntervalLength = length:CPDecimalFromFloat(10);
  x.constantCoordinateValue = length:CPDecimalFromFloat(2);
  x.minorTicksPerInterval = 2;
  x.borderWidth = 0;
  x.labelExclusionRanges = [NSArray arrayWithObjects:
                              [CPPlotRange plotRangeWithLocation:CPDecimalFromFloat(-100) 
                                                          length:CPDecimalFromFloat(300)], 
							  nil];;
 
  CPXYAxis *y = axisSet.yAxis;
  y.majorIntervalLength = length:CPDecimalFromFloat(10);
  y.minorTicksPerInterval = 1;
  y.constantCoordinateValue = length:CPDecimalFromFloat(2);
  y.labelExclusionRanges = [NSArray arrayWithObjects:
                              [CPPlotRange plotRangeWithLocation:CPDecimalFromFloat(-100) 
                                                          length:CPDecimalFromFloat(300)], 
							  nil];

Our graph is now fully set up – except for one important part: we still need to define one or more charts to display actual values. My application needs to show two graphs on top of each other – one showing the total number of tests that ran, and another showing the number of failures.

Core Plot supports a variety of chart (or, plot) types, such as bar and pie chart, but for this scenario, we want a simple line graph, which is done via a CPScatterPlot. (A scatter plot actually allows to draw a plot with points scattered all over the place, not just as string y = f(x) values, but we will use it in the more simplistic case).

We’ll create a new CPScatterPlot instance, configure some properties such as its lineWidth and lineColor, and most importantly, set the dataSource to the object that will provide the plot with its data – in this case, self. To round things off, we also tell the plot to fill the area underneath it with a gradient, going from almost-transparent green to nothing. This gives the plot some depth, without hiding the other plots.

  CPScatterPlot *dataSourceLinePlot = [[[CPScatterPlot alloc] init] autorelease];
  dataSourceLinePlot.identifier = @"AllTests";
  dataSourceLinePlot.dataLineStyle.lineWidth = 3.f;
  dataSourceLinePlot.dataLineStyle.lineColor = [CPColor greenColor];
  dataSourceLinePlot.dataSource = self;
  [graph addPlot:dataSourceLinePlot];
 
  // Put an area gradient under the plot above
  CPColor *areaColor = [CPColor colorWithComponentRed:0.3 
                                                green:1.0
                                                 blue:0.3
                                                alpha:0.3];
  CPGradient *areaGradient = [CPGradient gradientWithBeginningColor:areaColor 
                                                        endingColor:[CPColor clearColor]];
  areaGradient.angle = -90.0f;
  CPFill *areaGradientFill = [CPFill fillWithGradient:areaGradient];
  dataSourceLinePlot.areaFill = areaGradientFill;
  dataSourceLinePlot.areaBaseValue = CPDecimalFromString(@"1.75");

We repeat the above to add a second plot, except this time we use red for the line color and gradient, and set the identifier to @"FailedTests".

This was quite a bit setup, but our graph is now ready plot, and will ask its data source (our cell) for data by sending it messages from the CPPlotDataSource protocol. This protoco will be familiar to any Cocoa developer who worked with, for example, a UITableView or NSTableView, or any other control using a data source.

To the protocol, two methods need to be implemented. The first is numberOfRecordsForPlot: which will, simply enough, return the number of items (in case of a scatter plot, points, the chart will contain. The second can be one of three methods that return the actual data. We’ll implement numberForPlot:field:recordIndex:.

The data to display is retrieved via Data Abstract, using a simple DA SQL request on a table that contains all test runs:

	NSString *sql = [NSString stringWithFormat:@"SELECT TOP 50 ID, _TotalTestCount, \
                                     _FailedTestCount, FROM TestRuns ORDER BY ID DESC"];
	testRuns = [[rda getDataTableWithSQL:sql] retain];

The _TotalTestCount and _FailedTestCount fields are server-calculated based on a relation table and contain the total number of run and failed tests for each test run. We can Key Path operators to get the maximum value, and adjust our plot space accordingly, so that the entire graph fits into the chart:

  int count = [[testRuns rows] count];
  int maxTests = [[[testRuns rows] valueForKeyPath:@"@max._TotalTestCount"] intValue];
 
  CPXYPlotSpace *plotSpace = (CPXYPlotSpace *)graph.defaultPlotSpace;
  plotSpace.xRange = [CPPlotRange plotRangeWithLocation:CPDecimalFromFloat(0)
                                                 length:CPDecimalFromFloat(count)];
  plotSpace.yRange = [CPPlotRange plotRangeWithLocation:CPDecimalFromFloat(0)
                                                 length:CPDecimalFromFloat(maxTests)];

This is of course a bit redundant with the values we set earlier, but in real life application, the data access will usually happen in a different place than the initial setup (and data might get changed or refreshed during the course of the application), so i find it important to set up the graph properly to begin with, but then adjust the range to the real data.

With the data retrieved and the plot space adjusted, we can now implement the two data source methods.

numberOfRecordsForPlot: will simply return the number of rows in our table. numberForPlot:field:recordIndex will be called twice per data point, once for the X value (since we’re drawing a “non-scattered” plot, we’ll simply return the indexes) and one for the Y value, which we’ll retrieve from our data table. The different identifiers we assigned to the two plots will serve to distinguish which field to use for the Y value.

-(NSUInteger)numberOfRecordsForPlot:(CPPlot *)plot 
{
  return [rows count];
}
 
-(NSNumber *)numberForPlot:(CPPlot *)plot 
                     field:(NSUInteger)fieldEnum 
               recordIndex:(NSUInteger)index 
{
  switch (fieldEnum)
  {
    case CPScatterPlotFieldX: 
    {
        // inverse numbers, so first (latest) test run is on the right.
        int x = [rows count]-recordIndex; 
        return [NSDecimalNumber numberWithInt:x];
    }
    case CPScatterPlotFieldY:
    {
      if ([plot.identifier isEqual:@"AllTests"])
      {
        float v = [[[rows objectAtIndex:index] valueForKey:@"_TotalTestCount"] floatValue];
        return [NSNumber numberWithFloat:v];
      }
      else
      {
        float v = [[[rows objectAtIndex:index] valueForKey:@"_FailedTestCount"] floatValue];
        return [NSNumber numberWithFloat:v];
      }
    }
  }
  return nil;
}

And that’s it! You can find more info on Core Plot on their Google code page; i also recommend joining their mailing list for latest news and support.

26 responses to Visualizing Data with Core-Plot

  1. Great article! However, have you tried using multiple plotspaces? It seems that all the code examples I’ve seen only use the default plotspace. I’d love to see some example code that uses 2 plotspaces to plot things to different scales, for instance.

    • i have, yes. it’s pretty straight forward, i’ll try to put something together for you later today.

    • Sorry for the delay. Adding extra plot spaces is pretty straight-forward. First, you create and add them to the graph, for example like this

      timePlotSpace = [[[CPXYPlotSpace alloc] init] autorelease];
      [graph addPlotSpace:timePlotSpace];
      ratioPlotSpace = [[[CPXYPlotSpace alloc] init] autorelease];
      [graph addPlotSpace:ratioPlotSpace];
      

      And set them up just as you did the original one (setting range, etc) Secondly, you might wanna create an extra axis (but you don’t have to), and assign to it the right plotspace

      CPXYAxis *y2 = [[[CPXYAxis alloc] init] autorelease];
      y2.coordinate = CPCoordinateY;
      y2.tickDirection = CPSignPositive;
      y2.majorIntervalLength = CPDecimalFromInt(1.0);
      y2.minorTicksPerInterval = 0.1;
      y2.constantCoordinateValue = CPDecimalFromInt(dataPoints);
      y2.borderWidth = 0;
      y2.plotSpace = ratioPlotSpace;
      

      Finally, when you add your plot, use the overloaded addPlot:toPlotSpace: method. Important : DO NOT just assign the plotSpace property of the plot – that doesn;t seem to work. yo gotta ADD it to the right plotspace.

      [graph addPlot:dataSourceLinePlot toPlotSpace:ratioPlotSpace];
      

      hope that helps!

    • Can you send me an e-mail to follow up, I need to know if CorePlot can generate a graph like one that Excel has exported for me. I would have to show you the picture

    • that might be best done on the core-plot mailing list?

  2. Can you include a zipped up project file so we can try to run this sample?

  3. Hi Marc, could you also send me an example xcode-proj. with the application mentioned above?

    • Kevin, Patrick: the actual code snippets here are part of a larger project that unfortunately i cannot jst share as is (and if in could, you would not have the back-end data). i’ll see what i can do to wrap this into a smaller project i can share, over the next week or so (but pls be patient with me, coz we got a busy week ahead, here at RemObjects)

  4. Marc, Super Helpful! Your example pushed me in the right direction and over a hurdle I was having. Thanks for taking the time.

  5. Hi marc, good one.
    I need your help in adding one more point to the existing plot to extend the chart dynamically. how to achieve it with core plot. I need to draw real time plot.
    please help me..

  6. I did all the setups needed for Settings. But I get error for CPColor.h and CPLayer.h. All other header files are OK. Please advise. I am facing tight deadline.

  7. Hi Marc,

    Thanks for the tutorial. I tried downloading the Framework from the google site . But I get connection refused. Did you face any of this problem?

  8. Hi Marc,

    I am new to iPhone SDK and need to use core plot to display ECG data streaming via the dock connection for a school project. I have that dock connection sorted out and now testing the serial commuication, small problem but would be able to resolve. I am not sure how to get started to install core plot. I would really apprecaite that you can help to clarify a few question that I have:

    1) Must I install Mercurial?
    a) If YES then is this the correct site “http://mercurial.selenic.com/wiki/”?
    b) If YES, is this the correct version “2010-06-01 Mercurial 1.5.4 and TortoiseHg 1.0.4 released!”?

    2) How do I use “hg clone http://core-plot.googlecode.com/hg/ core-plot” to get the source?

    Your kins assistance is much apprecaited. Thank you.

    Irwan

    • Irwan,

      afaik core-plot is current;y only available via Mercurial checkout, yes. there’s no zip of the source or binary download i am aware of. any current version of Mercurial should work. Once Mercurial is installed, all you really need to do is run the “hg” command in Terminal.

      hth.

      • Hi Marc,

        Thanks for the reply. I finally manage to download core-plot and and run the tutorial on “http://www.switchonthecode.com/tutorials/using-core-plot-in-an-iphone-application” after much frustration to get it working with some modification due to the new Mac OS.

        Is it possible to do up a small example xcode-proj using core-plot with the following features:

        1) Input X Y value
        2) Press button to update graph
        3) Press another button to clear graph

        Or perhaps some xcode-proj that you have already that does similar application to help me with my school project.

        Your kind help is greatly appreciated. Thank you.

        Irwan

  9. Hi Marc,
    I am new to core-plot and tried out ur tutorial after setting up core plot in my iphone application. I wrote the code to include one line graph i.e until [graph addPlot:dataSourceLinePlot]; but no graph is drawn. I know the line graph cannot be drawn but i atleast expected the x and y coordinates. Am I missing anything?

    • it’s hard to tell, without more details. i suggest you post the full code you use to set up your graph to the core-plot mailing list.

  10. i write code to plot chart using core plot, but i getting only blank screen in the the i phone so please help me out in this

    thank
    manju

    //
    // Analytic.m
    // CoreLocationDemo
    //
    // Created by Manju on 9/15/10.
    // Copyright 2010 __MyCompanyName__. All rights reserved.
    //

    #import “Analytic.h”
    #import “CorePlot-CocoaTouch.h”

    @implementation Analytic
    @synthesize graph;

    - (id)initWithFrame:(CGRect)frame {

    if ((self = [super initWithFrame:frame])) {
    // Initialization code
    CPLayerHostingView *chartView = [[CPLayerHostingView alloc] initWithFrame: frame];
    [self addSubview:chartView];
    }

    // create an CPXYGraph and host it inside the view
    graph = [[CPXYGraph alloc] init];

    CPLayerHostingView *hostingView = [[CPLayerHostingView alloc]init];
    hostingView.hostedLayer = graph;
    graph.paddingLeft = 10.0;
    graph.paddingTop = 10.0;
    graph.paddingRight = 10.0;
    graph.paddingBottom = 10.0;

    CPXYPlotSpace *plotSpace = (CPXYPlotSpace *)graph.defaultPlotSpace;
    plotSpace.xRange = [CPPlotRange plotRangeWithLocation:CPDecimalFromFloat(0) length:CPDecimalFromFloat(100)];
    plotSpace.yRange = [CPPlotRange plotRangeWithLocation:CPDecimalFromFloat(0) length:CPDecimalFromFloat(100)];

    CPXYAxisSet *axisSet = (CPXYAxisSet *)graph.axisSet;
    CPLineStyle *lineStyle = [CPLineStyle lineStyle];
    lineStyle.lineColor = [CPColor blackColor];
    lineStyle.lineWidth = 2.0f;

    CPXYAxis *x = [[[CPXYAxis alloc] init] autorelease];
    x.coordinate = CPCoordinateX;
    x.tickDirection = CPSignPositive;
    x.majorIntervalLength = CPDecimalFromFloat(10);
    x.minorTicksPerInterval = 0.1;
    //x.constantCoordinateValue = CPDecimalFromInt(10);
    x.minorTicksPerInterval = 2;
    x.borderWidth = 0;
    x.labelExclusionRanges = [NSArray arrayWithObjects: [CPPlotRange plotRangeWithLocation:CPDecimalFromFloat(-100) length:CPDecimalFromFloat(300)], nil];

    CPXYAxis *y = [[[CPXYAxis alloc] init] autorelease];
    y.coordinate = CPCoordinateY;
    y.tickDirection = CPSignPositive;
    y.majorIntervalLength = CPDecimalFromFloat(10);
    y.minorTicksPerInterval = 0.1;
    //y.constantCoordinateValue = CPDecimalFromInt(10);
    y.minorTicksPerInterval = 2;
    y.borderWidth = 0;
    y.labelExclusionRanges = [NSArray arrayWithObjects: [CPPlotRange plotRangeWithLocation:CPDecimalFromFloat(-100) length:CPDecimalFromFloat(300)], nil];

    CPScatterPlot *xSquaredPlot = [[[CPScatterPlot alloc] init] autorelease];
    xSquaredPlot.identifier = @”X Squared Plot”;
    xSquaredPlot.dataLineStyle.lineWidth = 1.0f;
    xSquaredPlot.dataLineStyle.lineColor = [CPColor redColor];
    xSquaredPlot.dataSource = self;
    [graph addPlot:xSquaredPlot];

    return self;
    }

    -(NSUInteger)numberOfRecords {
    return 51;
    }

    -(NSNumber *)numberForPlot:(CPPlot *)plot field:(NSUInteger)fieldEnum
    recordIndex:(NSUInteger)index
    {
    double val = (index/5.0)-5;
    if(fieldEnum == CPScatterPlotFieldX)
    { return [NSNumber numberWithDouble:val]; }
    else
    {
    if(plot.identifier == @”X Squared Plot”)
    { return [NSNumber numberWithDouble:val*val]; }
    else
    { return [NSNumber numberWithDouble:1/val]; }
    }
    }

    // Only override drawRect: if you perform custom drawing.
    // An empty implementation adversely affects performance during animation.
    - (void)drawRect:(CGRect)rect {

    }

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

    @end

  11. Hi Marc
    I want to know if I can plot a multi-columns statcked bar graph like in link picture?

    Can you give me some hint if it is possible?

    http://www.geocities.jp/mikezang/iPad/graph.jpg

  12. Hi Marc,

    Thanks for the detailed post. CorePlot is a great piece of code, but can be a bit frustrating at times.

    Just wondered if you’d come across the kind of problem I’ve had below. If not, don’t worry about replying :-) I’ll get there in the end, but I was just browsing through your site at the time so thought I’d post….

    When data is changed I update three things on the graph:
    1) the data
    2) the y scale
    3) the paddingBottom of the graph

    1 and 2 cause no problems, but when I change the paddingBottom the top of the y scale goes screwy. I’m changing the padding because if the y scale starts at zero then the x axis labels fit below the graph (and need space), otherwise they are written over the graph (and don’t need the padding).

    I’m guessing that dynamically changing padding is not possible without totally recreating the graph object.

    Main thing I wanted to say was thanks for sharing the post at the top. Would be a bonus if my problem has a one line solution!

    Cheers

    Peter

  13. Thanks for this article ! I didn’t know some things like the areagradient ^^ By the way I wrote mine too : http://www.geckogeek.fr/installer-et-utiliser-coreplot-sur-iphone-ipad.html Good week !