ifish/Ifish/controllers/HotBar/Lib/PNChart/PNPieChart.m

512 lines
19 KiB
Objective-C
Executable File

//
// 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()<CAAnimationDelegate>
@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) {
if (!pdata.textDescription) {
break;
}
/* 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