Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

CALayer clip CGContextAddArc (making a donut slide/pie chart)

I'am trying to learn and understand CoreGraphics. What i'am trying to do is making a pie chart.

The pie chart is working fine and looks great, but i'am having trouble with clipping an inner circle.

This the code for each slide in the pie:

CGPoint center = CGPointMake((self.bounds.size.width/2) + self.centerOffset, (self.bounds.size.height/2) - self.centerOffset);
CGFloat radius = MIN(center.x, center.y) - 25;
radius *= self.pieScale;

CGContextBeginPath(ctx);
CGContextMoveToPoint(ctx, center.x, center.y);

CGPoint p1 = CGPointMake(center.x + radius * cosf(self.startAngle), center.y + radius * sinf(self.startAngle));
CGContextAddLineToPoint(ctx, p1.x, p1.y);

int clockwise = self.startAngle > self.endAngle;
CGContextAddArc(ctx, center.x, center.y, radius, self.startAngle, self.endAngle, clockwise);
CGContextClosePath(ctx);

CGContextMoveToPoint(ctx, center.x, center.y);
CGContextAddArc(ctx, center.x, center.y, radius*0.5, self.startAngle, self.endAngle, clockwise);

CGContextSetFillColorWithColor(ctx, self.fillColor.CGColor);
CGContextSetStrokeColorWithColor(ctx, self.strokeColor.CGColor);
CGContextSetLineWidth(ctx, self.strokeWidth);

self.pathRef = CGContextCopyPath(ctx);
CGContextDrawPath(ctx, kCGPathFillStroke);

My current pie looks like this:

http://cl.ly/image/1v1D3l3O0u3T

I managed to add an inner circle by drawing a new path with a smaller radius.

I try to clip the second path with CGContextClip(ctx); but leaves me only with the inner circle like this:

http://cl.ly/image/2G402n3G3J2G

It kind of makes sense to me why this is happening but i can't figure what else i should be doing.

Edit:

Code now looks like:

CGPoint center = CGPointMake((self.bounds.size.width/2) + self.centerOffset, (self.bounds.size.height/2) - self.centerOffset);
CGFloat radius = MIN(center.x, center.y) - 25;
radius *= self.pieScale;
CGPoint p1 = CGPointMake(center.x + radius * cosf(self.startAngle), center.y + radius * sinf(self.startAngle));
int clockwise = self.startAngle > self.endAngle;

CGContextBeginPath(ctx);
CGContextMoveToPoint(ctx, center.x, center.y);

CGContextAddLineToPoint(ctx, p1.x, p1.y);
CGContextAddArc(ctx, center.x, center.y, radius, self.startAngle, self.endAngle, clockwise);
CGContextClosePath(ctx);

CGContextMoveToPoint(ctx, center.x, center.y);
CGContextAddArc(ctx, center.x, center.y, radius*0.5, self.startAngle, self.endAngle, !clockwise);


CGContextSetFillColorWithColor(ctx, self.fillColor.CGColor);
CGContextSetStrokeColorWithColor(ctx, self.strokeColor.CGColor);
CGContextSetLineWidth(ctx, self.strokeWidth);

self.pathRef = CGContextCopyPath(ctx);
CGContextDrawPath(ctx, kCGPathFillStroke);

Looks like:

enter image description here

All drawing code:

My class is a subclass of CALayer. This code is drawing a single slice in the pie.

-(void)drawInContext:(CGContextRef)ctx
{
    CGPoint center = CGPointMake((self.bounds.size.width/2) + self.centerOffset, (self.bounds.size.height/2) - self.centerOffset);
    CGFloat radius = MIN(center.x, center.y) - 25;
    radius *= self.pieScale;
    int clockwise = self.startAngle > self.endAngle;

    /* Clipping should be done first so the next path(s) are not creating the clipping mask */
    CGContextMoveToPoint(ctx, center.x, center.y);
    CGContextAddArc(ctx, center.x, center.y, radius*0.5, self.startAngle, self.endAngle, !clockwise);
    //CGContextClipPath(ctx);
    CGContextClip(ctx);

    /* Now, start drawing your graph and filling things in... */
    CGContextBeginPath(ctx);
    CGContextMoveToPoint(ctx, center.x, center.y);

    CGPoint p1 = CGPointMake(center.x + radius * cosf(self.startAngle), center.y + radius * sinf(self.startAngle));

    CGContextAddLineToPoint(ctx, p1.x, p1.y);
    CGContextAddArc(ctx, center.x, center.y, radius, self.startAngle, self.endAngle, clockwise);
    CGContextClosePath(ctx);


    CGContextSetFillColorWithColor(ctx, self.fillColor.CGColor);
    CGContextSetStrokeColorWithColor(ctx, self.strokeColor.CGColor);
    CGContextSetLineWidth(ctx, self.strokeWidth);

    self.pathRef = CGContextCopyPath(ctx);
    CGContextDrawPath(ctx, kCGPathFillStroke);

    // LABELS
    UIGraphicsPushContext(ctx);
    CGContextSetFillColorWithColor(ctx, self.labelColor.CGColor);

    CGFloat distance = [self angleDistance:(self.startAngle * 180/M_PI) angle2:(self.endAngle * 180/M_PI)];
    CGFloat arcDistanceAngle = distance * M_PI/180;
    CGFloat arcCenterAngle = self.startAngle + arcDistanceAngle/2;

    CGPoint labelPoint = CGPointMake(center.x + radius * cosf(arcCenterAngle), center.y + radius * sinf(arcCenterAngle));


    /*
     Basic drawing of lines to labels.. Disabled for now..
    CGContextBeginPath(ctx);
    CGContextMoveToPoint(ctx, labelPoint.x, labelPoint.y);
    */

    if(labelPoint.x <= center.x)
        labelPoint.x -= 50;
    else
        labelPoint.x += 5;

    if(labelPoint.y <= center.y)
        labelPoint.y -= 25;



    /* 
     Basic drawing of lines to labels.. Disabled for now..
    CGContextAddLineToPoint(ctx, labelPoint.x, labelPoint.y);
    CGContextClosePath(ctx);

    CGContextSetFillColorWithColor(ctx, self.fillColor.CGColor);
    CGContextSetStrokeColorWithColor(ctx, self.strokeColor.CGColor);
    CGContextSetLineWidth(ctx, self.strokeWidth);
    CGContextDrawPath(ctx, kCGPathFillStroke);
    */

    [self.labelString drawAtPoint:labelPoint forWidth:50.0f withFont:[UIFont systemFontOfSize:18] lineBreakMode:NSLineBreakByClipping];
    UIGraphicsPopContext();

}
like image 354
Rasmus Styrk Avatar asked Sep 04 '13 12:09

Rasmus Styrk


2 Answers

If you don't want to have the center of the pie chart (i.e make a so called donut cart) then I would suggest that you create shapes that are the individual donut segments instead of making pie slices and masking the center.

The first thing that may come to mind is co create that segment out of two straight lines and two arcs with different radii but the same center coordinate. Luckily there are easier ways to do this shape in Core Graphics. This shape is really just a single arc between one angle and another but thicker. I've explained this all in this answer (to the question "Draw segments from a circle or donut") but here is a short explanation of the just the donut segment shape.

Step 1: Create an arc

Create the arc that will be in the center of the donut segment (the orange line in the below image). The radius will be (rmax + rmin) / 2.

enter image description here

CGMutablePathRef arc = CGPathCreateMutable();
CGPathMoveToPoint(arc, NULL,
                  startPoint.x, startPoint.y);
CGPathAddArc(arc, NULL,
             centerPoint.x, centerPoint.y,
             radius,
             startAngle,
             endAngle,
             YES);

Step 2: Stroke that arc

Create the final donut segment shape by stroking the arc. This function may look like magic to you but that is how it feels to find a hidden treasure in Core Graphics. It will stroke the path with a specific stroke width. "Line cap" and "line join" control how the start and end of the shape looks and how the joins between path components look (there is only one component in this shape).

enter image description here

CGFloat lineWidth = 10.0; // any radius you want
CGPathRef donutSegment =
    CGPathCreateCopyByStrokingPath(arc, NULL,
                                   lineWidth,
                                   kCGLineCapButt,
                                   kCGLineJoinMiter, // the default
                                   10); // 10 is default miter limit

Step 3: There is no step 3 (well, there is fill + stroke)

Fill this shape just like you did with the pie shapes. (lightGray and black was used in Image 2 (above)).

CGContextRef c = UIGraphicsGetCurrentContext();
CGContextAddPath(c, donutSegment);
CGContextSetFillColorWithColor(c, [UIColor lightGrayColor].CGColor);
CGContextSetStrokeColorWithColor(c, [UIColor blackColor].CGColor);
CGContextDrawPath(c, kCGPathFillStroke);
like image 162
David Rönnqvist Avatar answered Oct 12 '22 10:10

David Rönnqvist


If I understand your question correctly, you want to have a hole in the center and you are trying to use a clipping mask to do that. The only thing therefore you need to do is reverse the direction that you draw the inner path that you tried to clip. Then, because of fill rules, it will cut out a nice hole for you.

Your code should look something like this:

CGPoint center = CGPointMake((self.bounds.size.width/2) + self.centerOffset, (self.bounds.size.height/2) - self.centerOffset);
CGFloat radius = MIN(center.x, center.y) - 25;
radius *= self.pieScale;
int clockwise = self.startAngle > self.endAngle;

/* Clipping should be done first so the next path(s) are not creating the clipping mask */
CGContextMoveToPoint(ctx, center.x, center.y);
/* Create outer path going clockwise */
CGContextAddArc(ctx, center.x, center.y, radius*3, 0, 2*M_PI, clockwise);
/* Create inner path / mask going counter-clockwise to make the 'hole' in the center */
CGContextAddArc(ctx, center.x, center.y, radius*0.5, 0, 2*M_PI, !clockwise);
CGContextClip(ctx);

/* Now, start drawing your graph and filling things in... */
CGContextBeginPath(ctx);
/* Here's the stroke of the inner circle */
CGPoint p1 = CGPointMake(center.x + radius/2 * cosf(self.endAngle), center.y + radius/2 * sinf(self.endAngle));
CGContextMoveToPoint(ctx, p1.x, p1.y);
CGContextAddArc(ctx, center.x, center.y, radius / 2 + 1, self.endAngle, self.startAngle, !clockwise);

p1 = CGPointMake(center.x + radius * cosf(self.startAngle), center.y + radius * sinf(self.startAngle));

CGContextAddLineToPoint(ctx, p1.x, p1.y);
CGContextAddArc(ctx, center.x, center.y, radius, self.startAngle, self.endAngle, clockwise);
CGContextClosePath(ctx);


CGContextSetFillColorWithColor(ctx, self.fillColor.CGColor);
CGContextSetStrokeColorWithColor(ctx, self.strokeColor.CGColor);
CGContextSetLineWidth(ctx, self.strokeWidth);

self.pathRef = CGContextCopyPath(ctx);
CGContextDrawPath(ctx, kCGPathFillStroke);

Update: I've fixed the code and run it from your code on github and it works. Hopefully, this will fix your "hole" problem. (excuse the pun)

Here's the gist of an example of this technique: https://gist.github.com/rcdilorenzo/6437406

like image 45
Christian Di Lorenzo Avatar answered Oct 12 '22 09:10

Christian Di Lorenzo