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:
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:
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:
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();
}
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.
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
.
CGMutablePathRef arc = CGPathCreateMutable();
CGPathMoveToPoint(arc, NULL,
startPoint.x, startPoint.y);
CGPathAddArc(arc, NULL,
centerPoint.x, centerPoint.y,
radius,
startAngle,
endAngle,
YES);
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).
CGFloat lineWidth = 10.0; // any radius you want
CGPathRef donutSegment =
CGPathCreateCopyByStrokingPath(arc, NULL,
lineWidth,
kCGLineCapButt,
kCGLineJoinMiter, // the default
10); // 10 is default miter limit
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);
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With