2011年11月3日星期四

Creating a Graph With Quartz 2D: Part 2

Creating a Graph With Quartz 2D: Part 2:
In the first part of my series Creating a Graph with Quartz 2D I explained the background work. Bar graphs are a popular kind of graph, so let’s learn how to draw them.
First of all, I suggest commenting out the lines of code that draw the background image. We know how to do it if needed but let’s keep things as simple as possible here.
Second, it might be a good idea to leave some space between our bars, so let’s increase the horizontal step. In GraphView.h, modify the kStepX definition:
#define kStepX 70

Drawing Bars

Let’s add to GraphView.m a method that will draw one bar at a time. Make sure this method is defined before drawRect:
- (void)drawBar:(CGRect)rect context:(CGContextRef)ctx
{
}
We are passing a rectangle into the method, to fill it with the bar, and a graphics context to draw in. I find that a simple rectangle filled with a nice gradient works best for bars, so let’s learn how to draw one. If you prefer, you can modify the code that follows and draw, say, a rectangle with rounded corners but, once again, I prefer to keep things simple.
We are going to draw the rectangle as a path, therefore all the drawing code will be surrounded by the following two lines:
CGContextBeginPath(ctx);
...
CGContextClosePath(ctx);
Code for defining a gradient can be somewhat verbose, so to begin with, let’s fill our rectangles with a solid color. Here is the single line of code that will prepare the environment for drawing:
CGContextSetGrayFillColor(ctx, 0.2, 0.7);
The second parameter specifies how dark we want the fill to be, with 0 meaning black and 1 meaning white. In our case, it’s dark gray. The last parameter defines the transparency of the fill, with 0 being completely transparent and 1 being completely opaque. In our case, it’s 70% opaque.
The actual drawing takes four lines of code, and I believe you can easily guess what’s going on here from the names of the functions:
CGContextMoveToPoint(ctx, CGRectGetMinX(rect), CGRectGetMinY(rect));
CGContextAddLineToPoint(ctx, CGRectGetMaxX(rect), CGRectGetMinY(rect));
CGContextAddLineToPoint(ctx, CGRectGetMaxX(rect), CGRectGetMaxY(rect));
CGContextAddLineToPoint(ctx, CGRectGetMinX(rect), CGRectGetMaxY(rect));
Finally, in step 3, we need to commit what was drawn:
CGContextFillPath(ctx);
Here is the completed method for drawing a solid bar:
- (void)drawBar:(CGRect)rect context:(CGContextRef)ctx
{
CGContextBeginPath(ctx);
CGContextSetGrayFillColor(ctx, 0.2, 0.7);
CGContextMoveToPoint(ctx, CGRectGetMinX(rect), CGRectGetMinY(rect));
CGContextAddLineToPoint(ctx, CGRectGetMaxX(rect), CGRectGetMinY(rect));
CGContextAddLineToPoint(ctx, CGRectGetMaxX(rect), CGRectGetMaxY(rect));
CGContextAddLineToPoint(ctx, CGRectGetMinX(rect), CGRectGetMaxY(rect));
CGContextClosePath(ctx);
CGContextFillPath(ctx);
}

Graph Data

Next, we need to take care of the data displayed by the graph. Typically, the data could be delivered by some kind of web service. This could be, for example, the number of visitors of your website per month. However, for simplicity’s sake, we are going to hard-code the data, with the values between 0 and 1 where 1 will mean a bar taking the whole height of the graph and 0 meaning no bar at all. Place this line of code somewhere outside of any method in GraphView.m (the values are arbitrary, you can use any other):
float data[] = {0.7, 0.4, 0.9, 1.0, 0.2, 0.85, 0.11, 0.75, 0.53, 0.44, 0.88, 0.77};
Let’s add a couple of constants that will help us to position and size the bars:
#define kBarTop 10
#define kBarWidth 40
Finally, we need to draw the bars corresponding to the test values. In the very end of drawRect, place the following code:
// Draw the bars
float maxBarHeight = kGraphHeight - kBarTop - kOffsetY;
for (int i = 0; i < sizeof(data); i++)
{
float barX = kOffsetX + kStepX + i * kStepX - kBarWidth / 2;
float barY = kBarTop + maxBarHeight - maxBarHeight * data[i];
float barHeight = maxBarHeight * data[i];
CGRect barRect = CGRectMake(barX, barY, kBarWidth, barHeight);
[self drawBar:barRect context:context];
}
You should be able to understand what’s going on here without additional explanations, just bear in mind that Y coordinate increases from top to bottom. And here is the result that we’ve achieved so far:
Quartz 2D Part II Figure 1
Figure 1
The graph already looks quite good and can be useful for some applications as it is. You might want to use some other color instead of gray, but that’s easy to do. Here is a link to the CGContext Reference, where you will find all of the methods that you might need.
However, the graph will look dramatically better if we fill the bars with a gradient. Let’s see how this can be done.

Gradient Fills

The way that gradients are defined and used in Quartz is somewhat verbose, but it gives us a lot of power. Here is all we need to know to fill our bars with gradients.
First, we need to decide how many colors we are going to use for the gradient. We can use any number, but three colors should be sufficient for our purposes. Let’s define them by listing their red, green, blue and alpha components:
CGFloat components[12] = {0.2314, 0.5686, 0.4, 1.0,  // Start color
0.4727, 1.0, 0.8157, 1.0, // Second color
0.2392, 0.5686, 0.4118, 1.0}; // End color
Next, we need to decide where to position these colors in the gradient, with 0 meaning the beginning of the pattern, and 1 meaning the end of the pattern. Here is one possible distribution for our three colors:
CGFloat locations[3] = {0.0, 0.33, 1.0};
We’ll also need to explicitly define the number of locations:
size_t num_locations = 3;
Finally, we need to create a colorspace, and then, using all the prepared information, we can construct the gradient:
CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB();
CGGradientRef gradient = CGGradientCreateWithColorComponents(colorspace, components, locations, num_locations);
When you don’t need the gradient anymore, you should release both the gradient and the colorspace:
CGColorSpaceRelease(colorspace);
CGGradientRelease(gradient);
Just before using the gradient, we need to specify where the pattern will start and end, in terms of the graph space. We use the CGRect that was passed to the method to figure out these two points:
CGPoint startPoint = rect.origin;
CGPoint endPoint = CGPointMake(rect.origin.x + rect.size.width, rect.origin.y);
And, finally, here is the line that does the actual drawing:
// Draw the gradient
CGContextDrawLinearGradient(ctx, gradient, startPoint, endPoint, 0);
Here is all the code that prepares, draws and releases the gradient:
// Prepare the resources
CGFloat components[12] = {0.2314, 0.5686, 0.4, 1.0,  // Start color
0.4727, 1.0, 0.8157, 1.0, // Second color
0.2392, 0.5686, 0.4118, 1.0}; // End color
CGFloat locations[3] = {0.0, 0.33, 1.0};
size_t num_locations = 3;
CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB();
CGGradientRef gradient = CGGradientCreateWithColorComponents(colorspace, components, locations, num_locations);
CGPoint startPoint = rect.origin;
CGPoint endPoint = CGPointMake(rect.origin.x + rect.size.width, rect.origin.y);
// Draw the gradient
CGContextDrawLinearGradient(ctx, gradient, startPoint, endPoint, 0);
// Release the resources
CGColorSpaceRelease(colorspace);
CGGradientRelease(gradient);
At this point, we might be tempted to discard the code that we used before for drawing and filling the bar, and simply draw the gradient. If we do that, however, the result will be different to what we expected:
Quartz 2D Part II Figure 2
Figure 2
Looks like the gradient doesn’t really understand how much space it is supposed to take up. We need to somehow limit the drawing area to the dimensions of the bar. This is where clipping path becomes useful.

Clipping Paths

Here is how we are going to do it. First, we’ll draw the bar as a filled rectangle, like we did before, but instead of committing the drawing and making it visible, we’ll tell the graphics context: the bar we’ve just drawn defines the only space where you are allowed to draw from now on. This long phrase can be translated into a rather short line of code:
CGContextClip(ctx);
We should be able to lift the limitation immediately after the bar drawing is done. For this, we are going to tell the context to remember its state of unlimited freedom, right before applying the clipping path:
CGContextSaveGState(ctx);
And right after the gradient was drawn with the use of the clipping path, we are going to restore the initial state of the context:
CGContextRestoreGState(ctx);
Here is the complete solution for drawing a bar with a gradient fill, with all the steps that we mentioned above, in the correct order:
- (void)drawBar:(CGRect)rect context:(CGContextRef)ctx
{
// Prepare the resources
CGFloat components[12] = {0.2314, 0.5686, 0.4, 1.0,  // Start color
0.4727, 1.0, 0.8157, 1.0, // Second color
0.2392, 0.5686, 0.4118, 1.0}; // End color
CGFloat locations[3] = {0.0, 0.33, 1.0};
size_t num_locations = 3;
CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB();
CGGradientRef gradient = CGGradientCreateWithColorComponents(colorspace, components, locations, num_locations);
CGPoint startPoint = rect.origin;
CGPoint endPoint = CGPointMake(rect.origin.x + rect.size.width, rect.origin.y);
// Create and apply the clipping path
CGContextBeginPath(ctx);
CGContextSetGrayFillColor(ctx, 0.2, 0.7);
CGContextMoveToPoint(ctx, CGRectGetMinX(rect), CGRectGetMinY(rect));
CGContextAddLineToPoint(ctx, CGRectGetMaxX(rect), CGRectGetMinY(rect));
CGContextAddLineToPoint(ctx, CGRectGetMaxX(rect), CGRectGetMaxY(rect));
CGContextAddLineToPoint(ctx, CGRectGetMinX(rect), CGRectGetMaxY(rect));
CGContextClosePath(ctx);
CGContextSaveGState(ctx);
CGContextClip(ctx);
// Draw the gradient
CGContextDrawLinearGradient(ctx, gradient, startPoint, endPoint, 0);
CGContextRestoreGState(ctx);
// Release the resources
CGColorSpaceRelease(colorspace);
CGGradientRelease(gradient);
}

On Your Own

There is space for enhancement, of course. As we are using the same gradient again and again, it would be more efficient to create it just once and then reuse it for drawing as many bars as needed, rather than recreate the gradient for each bar. However, let me leave this refactoring to you. Here is what we should see when running this code:
Quartz 2D Part II Figure 3
Figure 3
We now have a bar graph that is close to completion. We’ll need some labels, and we’ll need to respond to touches but these topics will be covered in a later part of the series. Another popular kind of graph is a line graph, and in the next part of the series, we’ll learn how to draw those, including gradients, plus a few other nice tweaks.

Quartz 2D Index

Alexander Kolesnikov’s series on Creating a Graph using Quartz 2D was split into 5 parts. You can refer to the series using the Quartz 2D Tag and access the individual articles using the links below.

没有评论:

发表评论