13

I'm drawing a pie chart using a CAShapeLayer for each slice of the pie. Even when the end angle of one pie slice is equal to the start angle of the adjacent slice, antialiasing is resulting in the underlying background color appearing between each pice slice if the border between slices is at an angle.

https://i.stack.imgur.com/Qdu8d.png

I'd like to eliminate the slight gap between slices while still using antialiasing so the resulting pie cart still looks smooth. Conceptually, it seems if there were a way to apply antialiasing to the entire CALayer and it's pie slice sublayers after all the pie slices were drawn, that would do the trick... The pie slices would be antialiased into each other instead of into the background.

I've played around with as many CALayer properties as I can think of and am having a hard time finding more information on this. Any ideas?

UPDATE: See my answer below.

Aaron
  • 311
  • 2
  • 10

2 Answers2

9

You're probably not going to be able to make your pie slice edges meet up exactly with no cracks. The simplest solution is not to try.

Instead of making your pie slices meet at the edges, make them overlap. Draw the first slice as a full disc:

first slice

Then draw the second slice as a full disc, except for the proper area of the first slice:

second slice

Then draw the third slice as a full disc, except for the proper area of the first two slices:

third slice

And so on:

fourth slice fifth slice

Here's my code:

#import "PieView.h"

@implementation PieView {
    NSMutableArray *slices;
}

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        // Initialization code
    }
    return self;
}

- (void)layoutSubviews {
    [super layoutSubviews];
    [self layoutSliceLayers];
}

- (void)layoutSliceLayers {
    if (slices == nil) {
        [self createSlices];
    }
    [slices enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        [self layoutSliceLayer:obj index:idx];
    }];
}

static const int kSliceCount = 5;

- (void)createSlices {
    slices = [NSMutableArray arrayWithCapacity:kSliceCount];
    for (int i = 0; i < kSliceCount; ++i) {
        [slices addObject:[self newSliceLayerForIndex:i]];
    }
}

- (CAShapeLayer *)newSliceLayerForIndex:(int)i {
    CAShapeLayer *layer = [CAShapeLayer layer];
    layer.fillColor = [UIColor colorWithWhite:(CGFloat)i / kSliceCount alpha:1].CGColor;
    [self.layer addSublayer:layer];
    return layer;
}

- (void)layoutSliceLayer:(CAShapeLayer *)layer index:(int)index {
    layer.position = [self center];
    layer.path = [self pathForSliceIndex:index].CGPath;
}

- (CGPoint)center {
    CGRect bounds = self.bounds;
    return CGPointMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds));
}

- (UIBezierPath *)pathForSliceIndex:(int)i {
    CGFloat radius = [self radius];
    CGFloat fudgeRadians = 5 / radius;
    UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:CGPointZero
        radius:radius startAngle:2 * M_PI * i / kSliceCount
        endAngle:2 * M_PI clockwise:YES];
    [path addLineToPoint:CGPointZero];
    [path closePath];
    return path;
}

- (CGFloat)radius {
    CGSize size = self.bounds.size;
    return 0.9 * MIN(size.width, size.height) / 2;
}

@end
rob mayoff
  • 375,296
  • 67
  • 796
  • 848
  • Thanks a lot for the response, Rob. I did try this solution at one point and was seeing antialiasing artifacts from the darker, underlying layers being antialiased into the background. It appeared as a subtle dark edge around the pie chart that was especially noticeable at the outer edge of the lighter colored slices. The radial filler solution I've described seems to work well. – Aaron Dec 11 '13 at 17:29
  • @Aaron When you anti-alias, you end up with a ring of color+alpha pixels around an effectively-aliased shape of solid color. Doing this for multiple shapes with the same outline, your effectively-aliased inner area will look fine but your anti-aliased zone will have color(n)+alpha overlaying color(n-1)+alpha overlaying ... color(0)+alpha. This explains the effect you're seeing. – Brian Nickel Dec 11 '13 at 17:41
7

UPDATE: Rob's answer is pretty good, but possibly results in other antialiasing issues. I ended up 'filling' the gaps by drawing 1pt wide radial lines along the end angle of each pie slice, each line being the same color as the adjacent slice. These lines are drawn at a lower z index than the pie slices, so they are underneath. Here's what they look like without the pie slices drawn on top:

The filler lines with no slices drawn.

And here's the final product:

Final result with lines underneath and slices on top.

Aaron
  • 311
  • 2
  • 10