Creating a Graph With Quartz 2D: Part 2

Tweet
This entry is part 2 of 5 in the series Graphing with Quartz 2D

Graphing with Quartz 2D

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.

Graphing with Quartz 2D

<< Creating a Graph With Quartz 2DCreating a Graph With Quartz 2D: Part 3 >>

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • Sebastien Windal

    Thanks for this great tutorial, it helped me a lot to get started on my chart control!

    I believe there is an error which may is not visible in your code as posted (the extra junk points are just out of the chart area in your example)

    for (int i = 1; i < sizeof(data); i++)

    should be

    for (int i = 1; i < sizeof(data) / sizeof(float); i++)

    Thanks again.

    -Sebastien

    • http://siriuslab.com/ Alexander Kolesnikov

      You are right, Sebastien, that’s the correct way to do it. However, in this particular case it just works as written :)

  • raidu

    hi actually i didn’t get one thing that where did you take the y axis coordinates and also i want to print the y-axis coordinate similar to the x-axis coordinate i.e..(1,2,3,4…).
    and how can i take the multiple of 100 on x and y axis coordinate.

    ex : (10,150) , (150,30),etc… As i am a fresher the requirement i got is typical one can you please reply me.and is it possible to take on y axis the months of the year?

  • http://about.me/Arjun.Shivanand Arjun Shivanand Kannan

    Hi Alex

    Thanks for the lovely code. I’m just getting up to the bar graphs, and I’d like your help on the errors I’m getting. On this part of the code (within the for loop to draw the bars) :

    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];

    I get an error on every line –
    float barY … result of expression unused
    float barX … expected identifier
    float barHeight … result of expression unused
    CGRect barRect … expected identifier
    [self drawBar ... expected ]

    I even copy-pasted the exact code and got the same errors. Could you help me out ?

    I’m using XCode 4.5 with the iOS 6 SDK.

    Thanks a lot for your help !

    Arjun