Core Plot Design OverviewThis document describes the main classes of Core Plot, and how they work together. Design ConsiderationsBefore delving into the classes that make up Core Plot, it is worth considering the design goals of the framework. Core Plot has been developed to run on both Mac OS X and iPhone OS. This places some restrictions on the technologies that can be used: AppKit drawing is not possible, and view classes like NSView and UIView can only be used as host views. Drawing is instead performed using the low-level Quartz 2D API, and Core Animation layers are used to build up the various different aspects of a graph. It's not all bad news, because utilizing Core Animation also opens up a whole range of possibilities for introducing 'eye-candy'. Graphs can be animated, with transitions and 3D effects. The objective is to have Core Plot be capable of not only producing publication quality still images, but also stunning graphical effects and interactivity. Another objective that is influential in the design of Core Plot is that it should behave as much as possible from a developer's perspective as a built-in framework. Design patterns and technologies used in Apple's own frameworks, such as the data source pattern, delegation, and bindings, are all supported in Core Plot. Anatomy of a GraphThis diagram shows a standard bar graph with two data sets plotted. Below, the chart has been annotated to show the various components of the the chart, and the naming scheme used in Core Plot to identify them.
Class DiagramThis standard UML class diagram gives a static view of the main classes in the framework. The cardinality of relationships is given by a label, with a '1' indicating a to-one relationship, and an asterisk (*) representing a to-many relationship.
Objects and LayersThis diagram shows run time relationships between objects (right) together with layers in the Core Animation layer tree (left). Color coding shows the correspondence between objects and their corresponding layers.
LayersCore Animation's layer class, CALayer, is not very suitable for producing vector images, as required for publication quality graphics, and provides no support for event handling. For these reasons, Core Plot layers derive from a class called CPLayer, which itself is a subclass of CALayer. CPLayer includes drawing methods that make it possible to produce high quality vector graphics, as well as event handling methods to facilitate interaction. The drawing methods include -(void)renderAsVectorInContext:(CGContextRef)context;
-(void)recursivelyRenderInContext:(CGContextRef)context;
-(NSData *)dataForPDFRepresentationOfLayer; When subclassing CPLayer, it is important that you don't just override the standard drawInContext: method, but instead override renderAsVectorInContext:. That way, the layer will draw properly when vector graphics are generated, as well as when drawn to the screen. GraphsThe central class of Core Plot is CPGraph. In Core Plot, the term 'graph' refers to the complete diagram, which includes axes, labels, a title, and one or more plots (eg histogram, line plot). CPGraph is an abstract class from which all graph classes derive. A graph class is fundamentally a factory: It is responsible for creating the various objects that make up the graphic, and for setting up the appropriate relationships. The CPGraph class holds references to objects of other high level classes, such as CPAxisSet, CPPlotArea, and CPPlotSpace. It also keeps track of the plots (CPPlot instances) that are displayed on the graph. @interface CPGraph : CPLayer {
@protected
CPAxisSet *axisSet;
CPPlotArea *plotArea;
NSMutableArray *plots;
NSMutableArray *plotSpaces;
CPFill *fill;
}
@property (nonatomic, readwrite, retain) CPAxisSet *axisSet;
@property (nonatomic, readwrite, retain) CPPlotArea *plotArea;
@property (nonatomic, readonly, retain) CPPlotSpace *defaultPlotSpace;
@property (nonatomic, readwrite, retain) CPFill *fill;
// Retrieving plots
-(NSArray *)allPlots;
-(CPPlot *)plotAtIndex:(NSUInteger)index;
-(CPPlot *)plotWithIdentifier:(id <NSCopying>)identifier;
// Organizing plots
-(void)addPlot:(CPPlot *)plot;
-(void)addPlot:(CPPlot *)plot toPlotSpace:(CPPlotSpace *)space;
-(void)removePlot:(CPPlot *)plot;
-(void)insertPlot:(CPPlot*)plot atIndex:(NSUInteger)index;
-(void)insertPlot:(CPPlot*)plot atIndex:(NSUInteger)index intoPlotSpace:(CPPlotSpace *)space;
// Retrieving plot spaces
-(NSArray *)allPlotSpaces;
-(CPPlotSpace *)plotSpaceAtIndex:(NSUInteger)index;
-(CPPlotSpace *)plotSpaceWithIdentifier:(id <NSCopying>)identifier;
// Adding and removing plot spaces
-(void)addPlotSpace:(CPPlotSpace *)space;
-(void)removePlotSpace:(CPPlotSpace *)plotSpace;
@end
@interface CPGraph (AbstractFactoryMethods)
-(CPPlotSpace *)createPlotSpace;
-(CPAxisSet *)createAxisSet;
@endCPGraph is an abstract superclass; subclasses like CPXYGraph are actually responsible for doing most of creation and organization of graph components. Each subclass is usually associated with particular subclasses of the various layers that make up the graph. For example, the CPXYGraph creates an instance of CPXYAxisSet, and CPXYPlotSpace. This is a classic example of the Factory design pattern, as described in the GoF Design Patterns book (Gamma, et al). Plot AreaThe plot area is that part of a graph where data is plotted. It is typically bordered by axes, and grid lines may also appear in the plot area. There is only one plot area for each graph, and it is represented by the class CPPlotArea. Plot SpacesPlot spaces define the mapping between the coordinate space in which a set of data exists, and the drawing space inside the plot area. For example, if you were to plot the speed of a train versus time, the data space would have time along the horizontal axis, and speed on the vertical axis. The data space may range from 0 to 150 km/hr for the speed, and 0 to 180 minutes for the time. The drawing space, on the other hand, is dictated by the bounds of the plot area. A plot space, represented by a descendant of the CPPlotSpace class, defines the mapping between a coordinate in the data space, and the corresponding point in the plot area. It is tempting to use the built in support for affine transformations to perform the mapping between the data and drawing spaces, but this would be very limiting, because the mapping does not have to be linear. For example, it is not uncommon to use a logarithmic scale for the data space. To facilitate as wide a range of data sets as possible, all values in the data space are stored internally as NSDecimalNumber instances. It makes no sense to store values in the drawing space in this way, because drawing coordinates are represented in Cocoa by floating point numbers (CGFloat), and any extra precision would be lost. An CPPlotSpace subclass must implement two methods, one for transforming from drawing coordinates to data coordinates, and one for converting from data coordinates to drawing coordinates. -(CGPoint)viewPointForPlotPoint:(NSArray *)decimalNumbers;
-(NSArray *)plotPointForViewPoint:(CGPoint)point; Data coordinates --- represented here by the 'plot point' --- are passed as an array of NSDecimalNumber objects. Drawing coordinates --- represented here by the 'view point' --- are passed as standard CGPoint instances. Whenever an object needs to perform the transform from data to drawing coordinates, or vice versa, it should query the plot space to which it corresponds. For example, instances of CPPlot (discussed below) are each associated with a particular plot space, and use that plot space to determine where in the plot are they should draw. It is important to realize that a single graph may contain multiple plots, and that these plots may be plotted on different scales. For example, one plot may need to be drawn with a logarithmic scale, and a separate plot may be drawn on a linear scale. There is nothing to prevent both plots appearing in a single graph. For this reason, a single CPGraph instance can have multiple instances of CPPlotSpace. In the most common cases, there will only be a single instance of CPPlotSpace, but the flexibility exists within the framework to support multiple spaces in a single graph. PlotsA particular representation of data in a graph is known as a 'plot'. For example, data could be shown as a line or scatter plot, with a symbol at each data point. The same data could be represented by a bar plot/histogram. A graph can have multiple plots. Each plot can derive from a single data set, or different data sets: they are completely independent of one another. Although it may not seem like it at first glance, a plot is analogous to a table view. For example, to present a simple line plot of the speed of a train versus time, you need a value for the speed at different points in time. This data could be stored in two columns of a table view, or represented as a scatter plot. If effect, the plot and the table view are just different views of the same model data. What this means is that the same design patterns used to populate table views with data can be used to provide data to plots. In particular, you can either use the data source design pattern, or you can use bindings. To provide a plot with data using the data source approach, you set the dataSource outlet of the CPPlot object, and then implement the data source methods. @protocol CPPlotDataSource <NSObject>
-(NSUInteger)numberOfRecords;
@optional
// Implement one of the following
-(NSArray *)numbersForPlot:(CPPlot *)plot field:(NSUInteger)fieldEnum recordIndexRange:(NSRange)indexRange;
-(NSNumber *)numberForPlot:(CPPlot *)plot field:(NSUInteger)fieldEnum recordIndex:(NSUInteger)index;
-(NSRange)recordIndexRangeForPlot:(CPPlot *)plot plotRange:(CPPlotRange *)plotRect;
@end You can think of the field as being analogous to a column identifier in a table view, and the record index being analogous to the row index. Each type of plot has a fixed number of fields. For example, a scatter plot has two: the value of for the horizontal axis (x) and the value for the vertical axis (y). An enumerator in the CPScatterPlot class defines these fields. typedef enum _CPScatterPlotField {
CPScatterPlotFieldX,
CPScatterPlotFieldY
} CPScatterPlotField;A record is analogous to the row of a table view. For a scatter plot, it corresponds to a single point on the graph. Plot classes not only support the data source design pattern, but also Cocoa bindings, as a means of supplying data. This is again very similar to the approach taken with table views: each field of the plot --- analogous to a table column --- gets bound to a key path via an NSArrayController. CPGraph *graph = ...;
CPScatterPlot *boundLinePlot = [[[CPScatterPlot alloc] initWithFrame:CGRectZero] autorelease];
boundLinePlot.identifier = @"Bindings Plot";
boundLinePlot.dataLineStyle.lineWidth = 2.f;
[graph addPlot:boundLinePlot];
[boundLinePlot bind:CPScatterPlotBindingXValues toObject:self withKeyPath:@"arrangedObjects.x" options:nil];
[boundLinePlot bind:CPScatterPlotBindingYValues toObject:self withKeyPath:@"arrangedObjects.y" options:nil]; The superclass of all plot classes is CPPlot. This is an abstract base class; each subclass of CPPlot represents a particular variety of plot. For example, the CPScatterPlot class is used to draw line and scatter plots, while the CPBarPlot class is used for bar and histogram plots. A plot object has a close relationship to the CPPlotSpace class discussed earlier. In order to draw itself, the plot class needs to transform the values it receives from the data source into drawing coordinates. The plot space serves this purpose. AxesAxes describe the scale of the plotting coordinate space to the viewer. A basic graph will have just two axes, one for the horizontal direction (x) and one for the vertical direction (y), but this is not a constraint in Core Plot --- you can add as many axes as you like. Axes can appear at the sides of the plot area, but also on top of it. Axes can have different scales, and can include major and/or minor ticks, as well as labels and a title. Each axis on a graph is represented by an object of class descendant from CPAxis. CPAxis is responsible for drawing itself, and accessories like ticks and labels. To do this it needs to know how to map data coordinates into drawing coordinates. For this reason, each axis is associated with a single instance of CPPlotSpace. A graph can have multiple axes, but all axes get grouped together in a single CPAxisSet object. An axis set is a container for all the axes belonging to a graph, as well as a factory for creating standard sets of axes (eg CPXYAxisSet creates two axes, one for x and one for y). Axis labels are usually textual, but there is support in Core Plot for custom labels: any core animation layer can be used as an axis label by wrapping it in an instance of the CPAxisLabel class. AnimationsA unique aspect of Core Plot is the integration of animation, which facilitates dynamic effects and interactivity. The Core Animation framework provides the mechanisms for positioning and moving layers in time, but higher level classes are need to group these layer movements into a coherent animation of the graph as a whole, and to organize the flow from one animation to the next. (This is analogous to the way you build up a sequence of animations in an application like Keynote.) The main class of the animation component of Core Plot is CPAnimation. This class is fundamentally a state machine: it stores a graph of possible states (not to be confused with graphs of the CPGraph variety) --- known as key frames --- and the allowed transitions between those states.
It also provides the means for a user to control which transitions occur, and when they occur. This scheme is very flexible, and can be used for a variety of different applications, from basic non-interactive animation sequences to fully interactive, dynamic graphs. @interface CPAnimation : NSObject {
CPGraph *graph;
NSMutableSet *mutableKeyFrames;
NSMutableSet *mutableTransitions;
CPAnimationKeyFrame *currentKeyFrame;
}
@property (nonatomic, readonly, retain) CPGraph *graph;
@property (nonatomic, readonly, retain) NSSet *animationKeyFrames;
@property (nonatomic, readonly, retain) NSSet *animationTransitions;
@property (nonatomic, readonly, retain) CPAnimationKeyFrame *currentKeyFrame;
-(id)initWithGraph:(CPGraph *)graph;
// Key frames
-(void)addAnimationKeyFrame:(CPAnimationKeyFrame *)newKeyFrame;
-(CPAnimationKeyFrame *)animationKeyFrameWithIdentifier:(id <NSCopying>)identifier;
// Transitions
-(void)addAnimationTransition:(CPAnimationTransition *)newTransition fromKeyFrame:(CPAnimationKeyFrame *)startFrame toKeyFrame:(CPAnimationKeyFrame *)endFrame;
-(CPAnimationTransition *)animationTransitionWithIdentifier:(id <NSCopying>)identifier;
-(void)animationTransitionDidFinish:(CPAnimationTransition *)transition;
// Animating
-(void)performTransition:(CPAnimationTransition *)transition;
-(void)performTransitionToKeyFrame:(CPAnimationKeyFrame *)keyFrame;
@endThe nodes in the animation graph are objects of the class CPAnimationKeyFrame. This very simple class is not much more than a labeled state in the graph. An animation can pause at such a state, or continue on to another transition that is attached to the node. @interface CPAnimationKeyFrame : NSObject {
id <NSCopying> identifier;
BOOL isInitialFrame;
NSTimeInterval duration;
}
@property (nonatomic, readwrite, copy) id <NSCopying> identifier;
@property (nonatomic, readwrite, assign) NSTimeInterval duration;
-(id)initAsInitialFrame:(BOOL)isFirst;
@endThe final class, CPAnimationTransition, represents a single atomic part of an animation. For example, a transition could be the fading in of a particular plot. A complete CPAnimation might consist of many fade ins and fade outs, as well as other transitions. CPAnimationTransition is an abstract class; descendant classes are responsible for actually animating the components of the CPGraph using standard Core Animation techniques in the performTransition method. A CPAnimationTransition has a duration, and can be reversible, such that the transition can play forward or in reverse. A subclass returns YES from the getter method reversible if it can be used to transition in both directions. @interface CPAnimationTransition : NSObject {
id <NSCopying> identifier;
CPAnimationKeyFrame *startKeyFrame;
CPAnimationKeyFrame *endKeyFrame;
CPAnimationTransition *continuingTransition;
NSTimeInterval duration;
CPAnimation *animation;
}
@property (nonatomic, readwrite, assign) NSTimeInterval duration;
@property (nonatomic, readwrite, copy) id <NSCopying> identifier;
@property (nonatomic, readwrite, assign) CPAnimation *animation;
@property (nonatomic, readwrite, retain) CPAnimationKeyFrame *startKeyFrame;
@property (nonatomic, readwrite, retain) CPAnimationKeyFrame *endKeyFrame;
@property (nonatomic, readwrite, retain) CPAnimationTransition *continuingTransition;
@property (nonatomic, readonly, assign) BOOL reversible;
@end
@interface CPAnimationTransition (AbstractMethods)
-(void)performTransition;
@end
|