Cocoa for Scientists (Part XVIII): Graphing Data with SM2DGraphView
Author: Drew McCormack
Website: http://www.maccoremac.com
Last time we started a new app called Spectra, which could read in CSV data, present it in a table, allow the data to be edited, and save it out again in CSV format. In this tutorial, we are going to extend Spectra so that it can represent data graphically, using the SM2DGraphView framework, which is open-source software provided by Snowmint Creative Solutions LLC. (We will cover some other plotting frameworks in the near future, but SM2DGraphView is relatively simple to use and is well-documented, so it is a good place to start.) When all is said and done, we will have an app that can open and save CSV documents, supports viewing and editing of tabulated data, presents a graph of the data, and can even print the graph. Not bad for less than 200 lines of code.

Introducing SM2DGraphView
SM2DGraphView is a framework that has been around for many years, and has been used in a number of Cocoa applications, including 4Peaks from Mekentosj. Though it’s developed by a commercial firm, they have made it available under an open-source license, so you can use it in your own creations.
Before we can use SM2DGraphView in Spectra, we need to download and install it. Go to the SM2DGraphView web site and click the download link at the top of the page. You should end up with a disk image. Open it, and drag the SM2DGraphView.framework directory from the 10.4 Min folder to the ~/Library/Frameworks folder on your hard disk. We will also need the Interface Builder (IB) palette provided, so drag SM2DGraphView.palette from the 10.4 Min folder to the /Developer/Palettes folder on your hard disk. (If the folder doesn’t exist, just create it first.)
Including SM2DGraphView in Spectra
To add the SM2DGraphView framework to the Spectra project, begin by opening the Xcode project created last time. (You can download it here.) Then follow this procedure:
- Open the Frameworks group in the Groups & Files source list on the left, and select the Linked Frameworks group.
- Choose Project > Add to Project…
- Navigate to
~/Library/Frameworks/SM2DGraphView.frameworkand select it. Click Add. - In the sheet that appears, select ‘Default’ as the Reference Type, and make sure the Spectra target is checked.
- Click the Add button.
Now we need to add a new build phase that will copy the SM2DGraphView framework into the application bundle when it is built. If you don’t do this, and you distribute the app, SM2DGraphView will not be on the user’s computer, and your app won’t run.
- Open the Targets group in Groups & Files, and open the Spectra target by clicking the disclosure triangle.
- Right click on the Spectra target, and select Add > New Build Phase > New Copy Files Build Phase
- An Info panel will come up. Choose Frameworks in the Destination popup. Close the panel.
- Open the Frameworks and Linked Frameworks groups in Groups & Files, and drag the
SM2DGraphView.frameworkinto the newly created Copy Files build phase under the Spectra Target.
Interface Building
With the framework integrated into our Xcode project, we can turn to the Spectra interface. Double click the MyDocument.nib file in the Resources group of Groups & Files, and then make these changes:
- Select the window instance in
MyDocument.nib, and uncheck ‘Has texture’ in the Inspector. (To bring up the Inspector, use Command-Shift-I.) - Double click the Window instance in the
MyDocument.nibwindow, and select the Add button. - In the Inspector, change the button’s type to ‘Small Square Button’.
- Repeat for the Remove button.
- Resize the table in the document window so that it only occupies the bottom third of the space.
- With the the table selected, choose ‘Size’ from the popup button in the Inspector, and click the vertical inner spring to make it rigid (ie, straight), and the top outer spring to make it flexible (ie, curly).
The changes in (6) mean that the table will not resize vertically when the window resizes. Instead, we will have the graph resize with the window. (An alternative solution would be to use a split view.)
Last time we included Add and Remove buttons to alter the rows in the table. This time around, we will add a button, for refreshing the data displayed by the graph.
- Drag a glassy button from the top-left of the Cocoa-Controls palette (2nd from left) to the bottom-right of the document window.
- In the Inspector, choose Attributes from the popup button, and set the Type popup to ‘Small Square Button’.
- Double click the button and change the title to ‘Refresh’.
- With the button selected, choose ‘Size’ in the Inspector, and click on the top and left outside springs to make them curly. (This will cause the button to stick to the right and bottom of the window.)
Formatting Numbers
When we setup our table last time, we didn’t make any assumptions about what sort of data would be represented. In fact, you can enter any string in the Spectra table from the last tutorial.
In reality, we only want to be able to enter decimal numbers in the table. We can prevent arbitrary strings being entered by assigning an NSNumberFormatter to each column of the table view; this has the added bonus that whenever we retrieve data from the table, it will be returned in NSNumber instances, rather than just NSString objects.
To add formatters to the table:
- Open the Cocoa-Text palette, which is 3rd from the left.
- Drag a number formatter from the bottom-left of the palette onto the Energy table column. (The formatter icon has a dollar sign on it.)
- In the Inspector, make sure the Attributes pane is visible, and choose the fourth format from the top of the list (see screenshot below).
- Repeat these steps for the Intensity column.

Adding the Graph
Now we need a place to put the graph. We will add an NSBox to the upper two thirds of the window, and drop the graph view in it. The box is only for aesthetic purposes: it darkens the background a bit, and makes the graph look less out of place.
- Drag a box from the Cocoa-Containers palette, sixth from left. (If the palettes window is not visible, choose Tools > Palettes > Show Palettes.)
- In the Inspector, make sure the Attributes pane is selected, and uncheck the ‘Show Title’ checkbox.
- Resize the
NSBox: Make it wider than the window, and place it so that it extends beyond the sides so that you don’t see the rounded corners. You might find it easier to temporarily make the window bigger while you are resizing the box. - With the box selected, choose ‘Size’ from the Inspector popup button, and click on the two inner resizing springs to make them both curly.
To add the graph view to the box, proceed as follows:
- Select the SM2DGraphView palette (last) in the Palettes window.
- Double click the
NSBoxadded earlier to make its content editable. - Drag the graph in the top-left of the palette onto the box.
- Resize the graph to fit.
To test that the sizing behavior of the interface is all in order, choose File > Test Interface. To quit the test, use Command-Q.
Connecting up the Interface
The elements of the interface are all in place, but nothing is connected up yet. Let’s remedy that. We will need to define a few new actions and outlets, and then use the usual control-click and drag technique to connect it all up.
Let’s start with the graph view. We want to make the PointsArrayController instance act as the delegate and data source for the graph view.
- Control-click and drag from the SM2DGraphView to the PointsArrayController instance in the MyDocument.nib window.
- In the Inspector, choose the ‘delegate’ outlet, and click Connect.
- Choose the ‘dataSource’ outlet, and click Connect.
There will need to be some talking between the MyDocument instance, and the PointsArrayController instance. Here’s how to facilitate that:
- Click on the Classes tab of the MyDocument.nib window, and navigate to NSObject > NSDocument > MyDocument.
- Select the
MyDocumentclass, and bring up the Inspector. - Select ‘Attributes’ in the Inspector popup button, and click the Outlet tab.
- Click the Add button in the Inspector, and enter ‘pointsArrayController’ as the name of the new outlet.
- Repeat this for the
PointsArrayControllerclass, adding the outlets ‘document’ and ‘graphView’. - Click the Instances tab in the MyDocument.nib window.
- Control-click and drag from the PointsArrayController instance to File’s Owner — which is the
MyDocumentinstance — select the ‘document’ outlet in the Inspector, and click Connect. - Connect the ‘graphView’ outlet of PointsArrayController to the SM2DGraphView in the document window.
- Connect the ‘pointsArrayController’ outlet in File’s Owner to the PointsArrayController instance.
To finish this part, we need to add a couple of actions to PointsArrayController, and connect them up:
- Double click the PointsArrayController instance, which should take you to the class Inspector.
- Click the Actions tab in the Inspector.
- Add the action
refreshGraph:by clicking the Add button and entering the new action. - Select the Instances tab in the MyDocument.nib window.
- Control-click and drag from the Refresh button to the PointsArrayController instance, and connect it to the
refreshGraph:action.
Save your changes, and return to Xcode to add the special source.
The MyDocument class
We’ll begin with the MyDocument class. Much of the source code is the same as it was in the last tutorial, but I am including all of it here. Enter the following into MyDocument.h.
#import <Cocoa/Cocoa.h>
#import "PointsArrayController.h"
@interface MyDocument : NSDocument
{
NSArray *points;
IBOutlet PointsArrayController *pointsArrayController;
}
-(NSArray *)points;
-(void)setPoints:(NSArray *)newPoints;
@end
The class implementation, in MyDocument.m, should look like this:
#import "MyDocument.h"
@implementation MyDocument
#pragma mark -
#pragma mark Init and Dealloc
- (id)init
{
self = [super init];
if (self) {
[self setPoints:[NSArray array]];
}
return self;
}
#pragma mark -
#pragma mark Accessors
-(NSArray *)points {
return points;
}
-(void)setPoints:(NSArray *)newPoints {
[newPoints retain];
[points release];
points = newPoints;
}
#pragma mark -
#pragma mark Nib loading
- (NSString *)windowNibName
{
return @"MyDocument";
}
- (void)windowControllerDidLoadNib:(NSWindowController *) aController
{
[super windowControllerDidLoadNib:aController];
}
#pragma mark -
#pragma mark Reading and Writing
- (BOOL)writeToURL:(NSURL *)absoluteURL ofType:(NSString *)typeName error:(NSError **)outError
{
NSMutableString *csvString = [NSMutableString string];
NSEnumerator *pointEnum = [points objectEnumerator];
id point;
while ( point = [pointEnum nextObject] ) {
NSNumber *energy = [point valueForKey:@"energy"];
NSNumber *intensity = [point valueForKey:@"intensity"];
[csvString appendString:[NSString stringWithFormat:@"%@,%@\n", energy, intensity]];
}
return [csvString writeToURL:absoluteURL atomically:NO encoding:NSUTF8StringEncoding error:outError];
}
- (BOOL)readFromURL:(NSURL *)absoluteURL ofType:(NSString *)typeName error:(NSError **)outError
{
NSString *fileString = [NSString stringWithContentsOfURL:absoluteURL
encoding:NSUTF8StringEncoding error:outError];
if ( nil == fileString ) return NO;
NSScanner *scanner = [NSScanner scannerWithString:fileString];
[scanner setCharactersToBeSkipped:[NSCharacterSet characterSetWithCharactersInString:@"\n, "]];
NSMutableArray *newPoints = [NSMutableArray array];
float energy, intensity;
while ( [scanner scanFloat:&energy] && [scanner scanFloat:&intensity] ) {
[newPoints addObject:
[NSMutableDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithFloat:energy], @"energy",
[NSNumber numberWithFloat:intensity], @"intensity",
nil]];
}
[self setPoints:newPoints];
return YES;
}
#pragma mark -
#pragma mark Printing
-(IBAction)printDocument:(id)sender {
[pointsArrayController printGraph:self];
}
@end
This code is virtually unchanged from last time. There is one new method, printDocument:, which is used for printing the graph, but the rest of the class is as it was last time. We’ll discuss the printDocument: method — and how it gets invoked — a bit later.
The PointsArrayController class
The PointsArrayController class has changed much more than MyDocument. It includes the code necessary to fill the SM2DGraphView with data. Here is the PointsArrayController.h file:
#import <Cocoa/Cocoa.h>
#import <SM2DGraphView/SM2DGraphView.h>
@interface PointsArrayController : NSArrayController {
IBOutlet SM2DGraphView *graphView;
}
-(IBAction)refreshGraph:(id)sender;
-(IBAction)printGraph:(id)sender;
@end
The PointsArrayController class includes the graphView outlet defined earlier in IB, as well as the refreshGraph: action that we connected the Refresh button to. The printGraph: method gets called from the printDocument: method in MyDocument.m.
Most of the new code this week finds its way into PointsArrayController.m:
#import "PointsArrayController.h"
@implementation PointsArrayController
#pragma mark -
#pragma mark New Object Creation
-(id)newObject
{
return [[NSMutableDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithFloat:0.0], @"energy",
[NSNumber numberWithFloat:0.0], @"intensity",
nil] retain];
}
#pragma mark -
#pragma mark Actions
-(IBAction)refreshGraph:(id)sender
{
[graphView refreshDisplay:self];
}
-(IBAction)printGraph:(id)sender
{
[graphView print:self];
}
#pragma mark -
#pragma mark Graph View Data Source Methods
-(unsigned int)numberOfLinesInTwoDGraphView:(SM2DGraphView *)inGraphView
{
return 1;
}
-(NSArray *)twoDGraphView:(SM2DGraphView *)inGraphView dataForLineIndex:(unsigned int)inLineIndex
{
NSArray *points = [self arrangedObjects];
NSMutableArray *xyStrings = [NSMutableArray arrayWithCapacity:[points count]];
NSEnumerator *pointEnum = [points objectEnumerator];
id point;
while ( point = [pointEnum nextObject] ) {
NSPoint xyPoint = NSMakePoint([[point valueForKey:@"energy"] floatValue],
[[point valueForKey:@"intensity"] floatValue]);
[xyStrings addObject:NSStringFromPoint(xyPoint)];
}
return xyStrings;
}
-(double)twoDGraphView:(SM2DGraphView *)theGraphView maximumValueForLineIndex:(unsigned int)lineIndex
forAxis:(SM2DGraphAxisEnum)axis
{
NSArray *values;
if ( axis == kSM2DGraph_Axis_X )
values = [self valueForKeyPath:@"arrangedObjects.energy"];
else
values = [self valueForKeyPath:@"arrangedObjects.intensity"];
if ( [values count] == 0 ) return 1.0;
NSNumber *maxValue = [[values sortedArrayUsingSelector:@selector(compare:)] lastObject];
return [maxValue doubleValue];
}
-(double)twoDGraphView:(SM2DGraphView *)theGraphView minimumValueForLineIndex:(unsigned int)lineIndex
forAxis:(SM2DGraphAxisEnum)axis
{
NSArray *values;
if ( axis == kSM2DGraph_Axis_X )
values = [self valueForKeyPath:@"arrangedObjects.energy"];
else
values = [self valueForKeyPath:@"arrangedObjects.intensity"];
if ( [values count] == 0 ) return 0.0;
NSNumber *minValue = [[values sortedArrayUsingSelector:@selector(compare:)] objectAtIndex:0];
return [minValue doubleValue];
}
@end
The refreshGraph: action just invokes refreshDisplay: on the SM2DGraphView object, and, analogously, printGraph: just forwards the print request onto the SM2DGraphView instance via the print: method. The rest of the class is more involved, and is discussed more below.
You should now be able to build Spectra, and run it. Click the Build and Go toolbar button, and give it a try. You can download and build the source code if you haven’t got the patience to type it over. You can also download some sample data, unzip it, and open it in Spectra.
Data Sources
The SM2DGraphView class is based on a common Cocoa design pattern in which data is supplied via a callback mechanism by an object called the data source. This pattern is used, for example, by NSTableView (when not using Cocoa Bindings). The idea is that the view class (eg, NSTableView or SM2DGraphView) holds a pointer to a data source object; the data source object implements certain methods that supply data to the view when requested.
We connected the dataSource outlet of the SM2DGraphView in IB, and the data source methods are defined in PointsArrayController.m above. In particular, the methods numberOfLinesInTwoDGraphView:, twoDGraphView:dataForLineIndex:, twoDGraphView:maximumValueForLineIndex:, and twoDGraphView:maximumValueForLineIndex: are used to tell the SM2DGraphView how many plot lines there are on the graph, what data points are in each plot, and the range of each axis. There are others too, but they are not used in this case.
The numberOfLinesInTwoDGraphView: tells the graph how many plot lines should be included. In this case, we just want a single spectrum, so 1 is returned. twoDGraphView:dataForLineIndex: returns an array of strings holding the (x,y) points in the plot. The implementation of the method just loops over the points data array, and packs the values into strings for use by the graph view.
The last two methods are used by the graph view to determine the range of each axis. In this case, we just want to find the maximum and minimum x and y values in the data set, and use those to define the axis range. The implementations above use an NSArray method, sortedArrayUsingSelector:, to sort the data, and then return either the first or last value in the array as the minimum or maximum value. This is far from optimal, but adequate for such a simple application.
The Responder Chain and First Responder
There is still one aspect of Spectra that remains to be discussed in any detail: printing. Printing works via the so-called Responder Chain. This is a chain of objects that changes dynamically as an application runs. A message is sent along the chain until an object is found that understands what to do with it. The printDocument: message is one such example.
You’ll notice that we never actually connected up the Print… menu item. It was already connected to the First Responder in IB, and the printDocument: action in particular. First Responder is actually not a real object at all; it is actually a proxy for the responder chain. At any given time, the responder chain usually begins at a view in the active window, and then works its way up through the ancestor views to the window itself. After that it goes to the window controller, and eventually to the document and application.
We put a printDocument: method in the MyDocument class. When the user selects the Print… menu item, a printDocument: message is sent along the responder chain. It starts in the views, but because none of them have a printDocument: method, it continues along the chain. The NSWindow doesn’t have printDocument: either. Eventually the message gets to the MyDocument object of the active document, which does understand the message, and processes it. The request to print is passed on to the PointsArrayController instance, which in turn asks the SM2DGraphView to print itself.
The responder chain is not only used to handle printing, but is used for many aspects of Cocoa app design, including event handling (eg, key presses, mouse clicks). It often seems as if parts of a Cocoa app work by magic; in such cases, you can bet that the responder chain is playing a role. It’s also another example of how powerful dynamic languages like Objective-C can be: implementing a responder chain in a statically-typed language is not nearly as elegant.
Conclusions
Having built a graphing app in less than 200 lines of code, I think we can conclude that SM2DGraphView is a useful framework. There are some aspects of the framework’s design that don’t sit well with me. For example, it uses the data source pattern not only for plot data, but also for setting attributes like color and plotting symbol. But minor idiosyncrasies aside, it is a solid, well-documented, and reasonably complete framework. Next time we’ll take a look at a couple of alternatives to SM2DGraphView, and highlight strengths and weaknesses of each. Until then, don’t lose the plot. (You got this far, so you can at least grant me one bad pun.)



Comments
rounded numbers on axis?
SM2DGraphView is indeed a nice framework, I use it too in an app I wrote. Anyone knows of an algoritm how to get nice rounded numbers on the x and y axis?
Hi Drew
Hi Drew
Thanks for this nice tutorial. I have just two remarks:
(i) In the first tutorial you showed how to bind the "energy" and "intensity" columns to the NSArrayController, but in the tutorial you didn't mention to create the two keys in the attributes panel of the NSArrayController inspector pane..
(2) Why is it neccessary to set the PointsArrayController as the delegate of the graph view since you're not implementing any of the delegate methods?
Cheers
erigae
Axis rounding
Hi,
I have done such an algorithm!
It's wrapped up in an Cocoa object I call AxisModel. Have a look at the algorithm in the -interval method of this class (download the Xcode project to get the source).
This can be done with one for loop and one if statement, so not so hard really. It did take a few of us to figure it out in the first place!
Cheers,
Dan.
Questions...
Hi Erigae,
(i) Yes, you could add keys to the controller, but I don't think you have to do this to make it work.
(ii) You don't have to set the delegate, but it is not a bad idea, because in future you may need want to use the delegate methods, and in that case, chances are you will put them in the PointsArrayController. But you are right: it is not necessary in this case.
Drew
---------------------------
Drew McCormack
http://www.maccoremac.com
http://www.macanics.net
http://www.macresearch.org
Re: Rounding..
I took a look at the SM2DGraphView docs, but couldn't see any ways of influencing the tick positions along the axis, or force whole numbers to be used. Maybe something is in there, but I couldn't see it.
The Narrative plotting framework, which I developed a few years back, gives full control over tick placement, and also has an automatic algorithm for positioning ticks intelligently. I'll cover this framework next time.
Drew
---------------------------
Drew McCormack
http://www.maccoremac.com
http://www.macanics.net
http://www.macresearch.org
Re: Axis rounding
Thanks Dan, that looks promising. Now let's see if I can implement something similar with SM2DGraphView :)
Drew: Narrative looks very nice, but seems a bit overkill for my app. Thanks for the pointer, anyway.
Hi,
Hi,
The code for that algorithm is a bit messy still. I will try to tidy it up a bit tomorrow a post the new version.Update: Have a look here for an overview of axis rounding algorithms, including source code and documentation etc, http://www.boyfarrell.com/learning-curve/overview-of-axis-rounding
Cheers,
Dan.
SM2DGraphView.ibplugin problem
Hi,
I am using Xcode 3.0 for building the The SM2DGraphView framework including the SM2DGraphView.ibplugin plugin, but when I try to add the plugin to IB3 this message pops up:
The document “SM2DGraphView.ibplugin” could not be opened. The bundle is damaged or missing necessary resources.
Please help me.
Cheers,
Youldash
SM2DGraphView forums
I think you are better off asking this question on the SM2DGraphView forums:
http://www.snowmintcs.com/forums/viewforum.php?f=15&sid=b536051d1247bc4fd14cca875b98665d
---------------------------
Drew McCormack
http://www.maccoremac.com
http://www.macanics.net
http://www.macresearch.org
No window appears?!?
Great set of tutorials, Drew!
Weird problem. In the last tutorial, the simple Spectra program worked flawlessly. However, in this version, although everything builds without error or warning, no window pops up! The Spectra program appears to run, but there is nothing to interact with. Any ideas as to what is missing?
Thanks,
-Don
No windows
Hi Don,
I just downloaded the source from the tutorial, and compiled and ran it. It worked properly. I did have to remove the reference to the SM2DGraphView framework and re-add it to the project before it would compile for me.
Make sure you are opening the right project, not the one from last time. Also do a clean to make sure there aren't any artifacts from last time.
Other than that, it should work.
Drew
---------------------------
Drew McCormack
http://www.maccoremac.com
http://www.macanics.net
http://www.macresearch.org
No windows solution: needed to restart XCode
Hi Drew,
Thanks for the prompt feedback. It is strange. I had downloaded your source to compare with my typing---things were basically the same aside from some formatting style. Your source built and worked as expected.
I then quit and restarted XCode with my project. This time it worked fine. I suspect that there was something in the references to the framework that needed to be read "fresh". Thanks again!
-Don
Defining the source array
Hello,
I'm a cocoa newbie. I have been trying to figure out how the example above works, but I cannot figure out where it is written that "pointsArrayController" will draw the figure based on "points" array. Suppose that when I load the data file, the program creates to arrays, the second one with the same numbers but negative - which one would be displayed? Or what should I do if I want to have 2 graphs on the same view, but calling a different dataset each? I suppose this is defined there:
-(NSArray *)twoDGraphView:(SM2DGraphView *)inGraphView dataForLineIndex:(unsigned int)inLineIndex
and the key line is:
NSArray *points = [self arrangedObjects];
but after searching in the XCode help and on the web, I'm not able to understand what this does. Can someone help me?
Thanks so much
Defining the source array
OK, I found it, and I can reply to my own question if others encounter the same trouble. The critical point is to bind the pointsArrayController to the points array defined in MyDocument!!
Cannot build project, SM2DGraphView not found
Hello,
I have followed all the instructions of this tutorial, but everytime I try to compile, I get the following error:
"Command /Developer/usr/bin/gcc-4.0 failed with exit code 1
framework not found SM2DGraphView"
Can someone please direct me as to why this isn't being found?
Thanks.
Matt.
Re: SM2DGraphView not found
You can double click on the SM2DGraphView.framework in your Xcode project to see where Xcode thinks it should be. You can then either change the path to the framework, or simply remove it from the project, and re-add it (by dragging it in, for example.)
Drew
---------------------------
Drew McCormack
http://www.maccoremac.com
http://www.macanics.net
http://www.macresearch.org