//
|
// PNPieChart.m
|
// PNChartDemo
|
//
|
// Created by Hang Zhang on 14-5-5.
|
// Copyright (c) 2014年 kevinzhow. All rights reserved.
|
//
|
|
#import "PNPieChart.h"
|
//needed for the expected label size
|
#import "PNLineChart.h"
|
|
@interface PNPieChart()
|
|
@property (nonatomic) NSArray *items;
|
@property (nonatomic) NSArray *endPercentages;
|
|
@property (nonatomic) UIView *contentView;
|
@property (nonatomic) CAShapeLayer *pieLayer;
|
@property (nonatomic) NSMutableArray *descriptionLabels;
|
@property (strong, nonatomic) CAShapeLayer *sectorHighlight;
|
|
@property (nonatomic, strong) NSMutableDictionary *selectedItems;
|
|
- (void)loadDefault;
|
|
- (UILabel *)descriptionLabelForItemAtIndex:(NSUInteger)index;
|
- (PNPieChartDataItem *)dataItemForIndex:(NSUInteger)index;
|
- (CGFloat)startPercentageForItemAtIndex:(NSUInteger)index;
|
- (CGFloat)endPercentageForItemAtIndex:(NSUInteger)index;
|
- (CGFloat)ratioForItemAtIndex:(NSUInteger)index;
|
|
- (CAShapeLayer *)newCircleLayerWithRadius:(CGFloat)radius
|
borderWidth:(CGFloat)borderWidth
|
fillColor:(UIColor *)fillColor
|
borderColor:(UIColor *)borderColor
|
startPercentage:(CGFloat)startPercentage
|
endPercentage:(CGFloat)endPercentage;
|
|
|
@end
|
|
|
@implementation PNPieChart
|
|
-(id)initWithFrame:(CGRect)frame items:(NSArray *)items{
|
self = [self initWithFrame:frame];
|
if(self){
|
_items = [NSArray arrayWithArray:items];
|
[self baseInit];
|
}
|
|
return self;
|
}
|
|
- (void)awakeFromNib{
|
[super awakeFromNib];
|
[self baseInit];
|
}
|
|
- (void)baseInit{
|
_selectedItems = [NSMutableDictionary dictionary];
|
//在绘制圆形时,应当考虑矩形的宽和高的大小问题,当宽大于高时,绘制饼图时,会超出整个view的范围,因此建议在此处进行判断
|
|
CGFloat minimal = (CGRectGetWidth(self.bounds) < CGRectGetHeight(self.bounds)) ? CGRectGetWidth(self.bounds) : CGRectGetHeight(self.bounds);
|
|
_outerCircleRadius = minimal / 2;
|
_innerCircleRadius = minimal / 6;
|
// _outerCircleRadius = CGRectGetWidth(self.bounds) / 2;
|
// _innerCircleRadius = CGRectGetWidth(self.bounds) / 6;
|
_descriptionTextColor = [UIColor whiteColor];
|
_descriptionTextFont = [UIFont fontWithName:@"Avenir-Medium" size:18.0];
|
_descriptionTextShadowColor = [[UIColor blackColor] colorWithAlphaComponent:0.4];
|
_descriptionTextShadowOffset = CGSizeMake(0, 1);
|
_duration = 1.0;
|
_shouldHighlightSectorOnTouch = YES;
|
_enableMultipleSelection = NO;
|
_hideValues = NO;
|
|
[super setupDefaultValues];
|
[self loadDefault];
|
}
|
|
- (void)loadDefault{
|
__block CGFloat currentTotal = 0;
|
CGFloat total = [[self.items valueForKeyPath:@"@sum.value"] floatValue];
|
NSMutableArray *endPercentages = [NSMutableArray new];
|
[_items enumerateObjectsUsingBlock:^(PNPieChartDataItem *item, NSUInteger idx, BOOL *stop) {
|
if (total == 0){
|
[endPercentages addObject:@(1.0 / _items.count * (idx + 1))];
|
}else{
|
currentTotal += item.value;
|
[endPercentages addObject:@(currentTotal / total)];
|
}
|
}];
|
self.endPercentages = [endPercentages copy];
|
|
[_contentView removeFromSuperview];
|
_contentView = [[UIView alloc] initWithFrame:self.bounds];
|
[self addSubview:_contentView];
|
_descriptionLabels = [NSMutableArray new];
|
|
_pieLayer = [CAShapeLayer layer];
|
[_contentView.layer addSublayer:_pieLayer];
|
|
}
|
|
/** Override this to change how inner attributes are computed. **/
|
- (void)recompute {
|
|
//同理
|
CGFloat minimal = (CGRectGetWidth(self.bounds) < CGRectGetHeight(self.bounds)) ? CGRectGetWidth(self.bounds) : CGRectGetHeight(self.bounds);
|
self.outerCircleRadius = minimal / 2;
|
self.innerCircleRadius = minimal / 6;
|
}
|
|
#pragma mark -
|
|
- (void)strokeChart{
|
[self loadDefault];
|
[self recompute];
|
|
PNPieChartDataItem *currentItem;
|
for (int i = 0; i < _items.count; i++) {
|
currentItem = [self dataItemForIndex:i];
|
|
|
CGFloat startPercentage = [self startPercentageForItemAtIndex:i];
|
CGFloat endPercentage = [self endPercentageForItemAtIndex:i];
|
|
CGFloat radius = _innerCircleRadius + (_outerCircleRadius - _innerCircleRadius) / 2;
|
CGFloat borderWidth = _outerCircleRadius - _innerCircleRadius;
|
|
CAShapeLayer *currentPieLayer = [self newCircleLayerWithRadius:radius
|
borderWidth:borderWidth
|
fillColor:[UIColor clearColor]
|
borderColor:currentItem.color
|
startPercentage:startPercentage
|
endPercentage:endPercentage];
|
[_pieLayer addSublayer:currentPieLayer];
|
}
|
|
[self maskChart];
|
|
for (int i = 0; i < _items.count; i++) {
|
UILabel *descriptionLabel = [self descriptionLabelForItemAtIndex:i];
|
[_contentView addSubview:descriptionLabel];
|
[_descriptionLabels addObject:descriptionLabel];
|
}
|
|
[self addAnimationIfNeeded];
|
}
|
|
- (UILabel *)descriptionLabelForItemAtIndex:(NSUInteger)index{
|
PNPieChartDataItem *currentDataItem = [self dataItemForIndex:index];
|
CGFloat distance = _innerCircleRadius + (_outerCircleRadius - _innerCircleRadius) / 2;
|
CGFloat centerPercentage = ([self startPercentageForItemAtIndex:index] + [self endPercentageForItemAtIndex:index])/ 2;
|
CGFloat rad = centerPercentage * 2 * M_PI;
|
|
UILabel *descriptionLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 100, 80)];
|
NSString *titleText = currentDataItem.textDescription;
|
|
NSString *titleValue;
|
|
if (self.showAbsoluteValues) {
|
titleValue = [NSString stringWithFormat:@"%.0f",currentDataItem.value];
|
}else{
|
titleValue = [NSString stringWithFormat:@"%.0f%%",[self ratioForItemAtIndex:index] * 100];
|
}
|
|
if (self.hideValues)
|
descriptionLabel.text = titleText;
|
else if(!titleText || self.showOnlyValues)
|
descriptionLabel.text = titleValue;
|
else {
|
NSString* str = [titleValue stringByAppendingString:[NSString stringWithFormat:@"\n%@",titleText]];
|
descriptionLabel.text = str ;
|
}
|
|
//If value is less than cutoff, show no label
|
if ([self ratioForItemAtIndex:index] < self.labelPercentageCutoff )
|
{
|
descriptionLabel.text = nil;
|
}
|
|
CGPoint center = CGPointMake(_outerCircleRadius + distance * sin(rad),
|
_outerCircleRadius - distance * cos(rad));
|
|
descriptionLabel.font = _descriptionTextFont;
|
CGSize labelSize = [descriptionLabel.text sizeWithAttributes:@{NSFontAttributeName:descriptionLabel.font}];
|
descriptionLabel.frame = CGRectMake(descriptionLabel.frame.origin.x, descriptionLabel.frame.origin.y,
|
descriptionLabel.frame.size.width, labelSize.height);
|
descriptionLabel.numberOfLines = 0;
|
descriptionLabel.textColor = _descriptionTextColor;
|
descriptionLabel.shadowColor = _descriptionTextShadowColor;
|
descriptionLabel.shadowOffset = _descriptionTextShadowOffset;
|
descriptionLabel.textAlignment = NSTextAlignmentCenter;
|
descriptionLabel.center = center;
|
descriptionLabel.alpha = 0;
|
descriptionLabel.backgroundColor = [UIColor clearColor];
|
return descriptionLabel;
|
}
|
|
- (void)updateChartData:(NSArray *)items {
|
self.items = items;
|
}
|
|
- (PNPieChartDataItem *)dataItemForIndex:(NSUInteger)index{
|
return self.items[index];
|
}
|
|
- (CGFloat)startPercentageForItemAtIndex:(NSUInteger)index{
|
if(index == 0){
|
return 0;
|
}
|
|
return [_endPercentages[index - 1] floatValue];
|
}
|
|
- (CGFloat)endPercentageForItemAtIndex:(NSUInteger)index{
|
return [_endPercentages[index] floatValue];
|
}
|
|
- (CGFloat)ratioForItemAtIndex:(NSUInteger)index{
|
return [self endPercentageForItemAtIndex:index] - [self startPercentageForItemAtIndex:index];
|
}
|
|
#pragma mark private methods
|
|
- (CAShapeLayer *)newCircleLayerWithRadius:(CGFloat)radius
|
borderWidth:(CGFloat)borderWidth
|
fillColor:(UIColor *)fillColor
|
borderColor:(UIColor *)borderColor
|
startPercentage:(CGFloat)startPercentage
|
endPercentage:(CGFloat)endPercentage{
|
CAShapeLayer *circle = [CAShapeLayer layer];
|
|
CGPoint center = CGPointMake(CGRectGetMidX(self.bounds),CGRectGetMidY(self.bounds));
|
|
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:center
|
radius:radius
|
startAngle:-M_PI_2
|
endAngle:M_PI_2 * 3
|
clockwise:YES];
|
|
circle.fillColor = fillColor.CGColor;
|
circle.strokeColor = borderColor.CGColor;
|
circle.strokeStart = startPercentage;
|
circle.strokeEnd = endPercentage;
|
circle.lineWidth = borderWidth;
|
circle.path = path.CGPath;
|
|
return circle;
|
}
|
|
- (void)maskChart{
|
CGFloat radius = _innerCircleRadius + (_outerCircleRadius - _innerCircleRadius) / 2;
|
CGFloat borderWidth = _outerCircleRadius - _innerCircleRadius;
|
CAShapeLayer *maskLayer = [self newCircleLayerWithRadius:radius
|
borderWidth:borderWidth
|
fillColor:[UIColor clearColor]
|
borderColor:[UIColor blackColor]
|
startPercentage:0
|
endPercentage:1];
|
|
_pieLayer.mask = maskLayer;
|
}
|
|
- (void)addAnimationIfNeeded{
|
if (self.displayAnimated) {
|
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
|
animation.duration = _duration;
|
animation.fromValue = @0;
|
animation.toValue = @1;
|
animation.delegate = self;
|
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
|
animation.removedOnCompletion = YES;
|
[_pieLayer.mask addAnimation:animation forKey:@"circleAnimation"];
|
}
|
else {
|
// Add description labels since no animation is required
|
[_descriptionLabels enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
|
[obj setAlpha:1];
|
}];
|
}
|
}
|
|
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag{
|
[_descriptionLabels enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
|
[UIView animateWithDuration:0.2 animations:^(){
|
[obj setAlpha:1];
|
}];
|
}];
|
}
|
|
- (void)didTouchAt:(CGPoint)touchLocation
|
{
|
CGPoint circleCenter = CGPointMake(_contentView.bounds.size.width/2, _contentView.bounds.size.height/2);
|
|
CGFloat distanceFromCenter = sqrtf(powf((touchLocation.y - circleCenter.y),2) + powf((touchLocation.x - circleCenter.x),2));
|
|
if (distanceFromCenter < _innerCircleRadius) {
|
if ([self.delegate respondsToSelector:@selector(didUnselectPieItem)]) {
|
[self.delegate didUnselectPieItem];
|
}
|
[self.sectorHighlight removeFromSuperlayer];
|
return;
|
}
|
|
CGFloat percentage = [self findPercentageOfAngleInCircle:circleCenter fromPoint:touchLocation];
|
int index = 0;
|
while (percentage > [self endPercentageForItemAtIndex:index]) {
|
index ++;
|
}
|
|
if ([self.delegate respondsToSelector:@selector(userClickedOnPieIndexItem:)]) {
|
[self.delegate userClickedOnPieIndexItem:index];
|
}
|
|
if (self.shouldHighlightSectorOnTouch)
|
{
|
if (!self.enableMultipleSelection)
|
{
|
if (self.sectorHighlight)
|
[self.sectorHighlight removeFromSuperlayer];
|
}
|
|
PNPieChartDataItem *currentItem = [self dataItemForIndex:index];
|
|
CGFloat red,green,blue,alpha;
|
UIColor *old = currentItem.color;
|
[old getRed:&red green:&green blue:&blue alpha:&alpha];
|
alpha /= 2;
|
UIColor *newColor = [UIColor colorWithRed:red green:green blue:blue alpha:alpha];
|
|
CGFloat startPercentage = [self startPercentageForItemAtIndex:index];
|
CGFloat endPercentage = [self endPercentageForItemAtIndex:index];
|
|
self.sectorHighlight = [self newCircleLayerWithRadius:_outerCircleRadius + 5
|
borderWidth:10
|
fillColor:[UIColor clearColor]
|
borderColor:newColor
|
startPercentage:startPercentage
|
endPercentage:endPercentage];
|
|
if (self.enableMultipleSelection)
|
{
|
NSString *dictIndex = [NSString stringWithFormat:@"%d", index];
|
CAShapeLayer *indexShape = [self.selectedItems valueForKey:dictIndex];
|
if (indexShape)
|
{
|
[indexShape removeFromSuperlayer];
|
[self.selectedItems removeObjectForKey:dictIndex];
|
}
|
else
|
{
|
[self.selectedItems setObject:self.sectorHighlight forKey:dictIndex];
|
[_contentView.layer addSublayer:self.sectorHighlight];
|
}
|
}
|
else
|
{
|
[_contentView.layer addSublayer:self.sectorHighlight];
|
}
|
}
|
}
|
|
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
|
{
|
for (UITouch *touch in touches) {
|
CGPoint touchLocation = [touch locationInView:_contentView];
|
[self didTouchAt:touchLocation];
|
}
|
}
|
|
- (CGFloat) findPercentageOfAngleInCircle:(CGPoint)center fromPoint:(CGPoint)reference{
|
//Find angle of line Passing In Reference And Center
|
CGFloat angleOfLine = atanf((reference.y - center.y) / (reference.x - center.x));
|
CGFloat percentage = (angleOfLine + M_PI/2)/(2 * M_PI);
|
return (reference.x - center.x) > 0 ? percentage : percentage + .5;
|
}
|
|
- (UIView*) getLegendWithMaxWidth:(CGFloat)mWidth{
|
if ([self.items count] < 1) {
|
return nil;
|
}
|
|
/* This is a small circle that refers to the chart data */
|
CGFloat legendCircle = 16;
|
|
CGFloat hSpacing = 0;
|
|
CGFloat beforeLabel = legendCircle + hSpacing;
|
|
/* x and y are the coordinates of the starting point of each legend item */
|
CGFloat x = 0;
|
CGFloat y = 0;
|
|
/* accumulated width and height */
|
CGFloat totalWidth = 0;
|
CGFloat totalHeight = 0;
|
|
NSMutableArray *legendViews = [[NSMutableArray alloc] init];
|
|
/* Determine the max width of each legend item */
|
CGFloat maxLabelWidth;
|
if (self.legendStyle == PNLegendItemStyleStacked) {
|
maxLabelWidth = mWidth - beforeLabel;
|
}else{
|
maxLabelWidth = MAXFLOAT;
|
}
|
|
/* this is used when labels wrap text and the line
|
* should be in the middle of the first row */
|
CGFloat singleRowHeight = [PNLineChart sizeOfString:@"Test"
|
withWidth:MAXFLOAT
|
font:self.legendFont ? self.legendFont : [UIFont systemFontOfSize:12.0f]].height;
|
|
NSUInteger counter = 0;
|
NSUInteger rowWidth = 0;
|
NSUInteger rowMaxHeight = 0;
|
|
for (PNPieChartDataItem *pdata in self.items) {
|
/* Expected label size*/
|
CGSize labelsize = [PNLineChart sizeOfString:pdata.textDescription
|
withWidth:maxLabelWidth
|
font:self.legendFont ? self.legendFont : [UIFont systemFontOfSize:12.0f]];
|
|
if ((rowWidth + labelsize.width + beforeLabel > mWidth)&&(self.legendStyle == PNLegendItemStyleSerial)) {
|
rowWidth = 0;
|
x = 0;
|
y += rowMaxHeight;
|
rowMaxHeight = 0;
|
}
|
rowWidth += labelsize.width + beforeLabel;
|
totalWidth = self.legendStyle == PNLegendItemStyleSerial ? fmaxf(rowWidth, totalWidth) : fmaxf(totalWidth, labelsize.width + beforeLabel);
|
// Add inflexion type
|
[legendViews addObject:[self drawInflexion:legendCircle * .6
|
center:CGPointMake(x + legendCircle / 2, y + singleRowHeight / 2)
|
andColor:pdata.color]];
|
|
|
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(x + beforeLabel, y, labelsize.width, labelsize.height)];
|
label.text = pdata.textDescription;
|
label.textColor = self.legendFontColor ? self.legendFontColor : [UIColor blackColor];
|
label.font = self.legendFont ? self.legendFont : [UIFont systemFontOfSize:12.0f];
|
label.lineBreakMode = NSLineBreakByWordWrapping;
|
label.numberOfLines = 0;
|
|
|
rowMaxHeight = fmaxf(rowMaxHeight, labelsize.height);
|
x += self.legendStyle == PNLegendItemStyleStacked ? 0 : labelsize.width + beforeLabel;
|
y += self.legendStyle == PNLegendItemStyleStacked ? labelsize.height : 0;
|
|
|
totalHeight = self.legendStyle == PNLegendItemStyleSerial ? fmaxf(totalHeight, rowMaxHeight + y) : totalHeight + labelsize.height;
|
[legendViews addObject:label];
|
counter ++;
|
}
|
|
UIView *legend = [[UIView alloc] initWithFrame:CGRectMake(0, 0, totalWidth, totalHeight)];
|
|
for (UIView* v in legendViews) {
|
[legend addSubview:v];
|
}
|
return legend;
|
}
|
|
|
- (UIImageView*)drawInflexion:(CGFloat)size center:(CGPoint)center andColor:(UIColor*)color
|
{
|
//Make the size a little bigger so it includes also border stroke
|
CGSize aSize = CGSizeMake(size, size);
|
|
|
UIGraphicsBeginImageContextWithOptions(aSize, NO, 0.0);
|
CGContextRef context = UIGraphicsGetCurrentContext();
|
|
CGContextAddArc(context, size/2, size/ 2, size/2, 0, M_PI*2, YES);
|
|
|
//Set some fill color
|
CGContextSetFillColorWithColor(context, color.CGColor);
|
|
//Finally draw
|
CGContextDrawPath(context, kCGPathFill);
|
|
//now get the image from the context
|
UIImage *squareImage = UIGraphicsGetImageFromCurrentImageContext();
|
|
UIGraphicsEndImageContext();
|
|
//// Translate origin
|
CGFloat originX = center.x - (size) / 2.0;
|
CGFloat originY = center.y - (size) / 2.0;
|
|
UIImageView *squareImageView = [[UIImageView alloc]initWithImage:squareImage];
|
[squareImageView setFrame:CGRectMake(originX, originY, size, size)];
|
return squareImageView;
|
}
|
|
/* Redraw the chart on autolayout */
|
-(void)layoutSubviews {
|
[super layoutSubviews];
|
[self strokeChart];
|
}
|
|
@end
|