iOS 南丁格尔玫瑰图+指引线

255 阅读2分钟

截屏2022-03-25 下午3.23.41.png

PieChartView *pieChartView = [[PieChartView alloc] initWithFrame:CGRectMake(0, 0, 300, 500)];
pieChartView.backgroundColor = [UIColor whiteColor];
pieChartView.dataArray =  @[@"120", @"150", @"432", @"500", @"100", @"300"];
pieChartView.detailTextArray = @[@"数据1", @"数据2", @"数据3", @"数据4", @"数据5", @"数据6"];
pieChartView.colorArray = @[
    [UIColor colorWithRed:251/255.0 green:166.9/255.0 blue:96.5/255.0 alpha:1],
    [UIColor colorWithRed:151.9/255.0 green:188/255.0 blue:95.8/255.0 alpha:1],
    [UIColor colorWithRed:245/255.0 green:94/255.0 blue:102/255.0 alpha:1],
    [UIColor colorWithRed:29/255.0 green:140/255.0 blue:140/255.0 alpha:1],
    [UIColor colorWithRed:121/255.0 green:113/255.0 blue:199/255.0 alpha:1],
    [UIColor colorWithRed:16/255.0 green:149/255.0 blue:224/255.0 alpha:1]
];
[self.view addSubview:pieChartView];
[pieChartView strokePath];
@interface PieChartView : UIView
@property (nonatomic, strong) NSArray *dataArray;//数据数组
@property (nonatomic, strong) NSArray *colorArray;//颜色数组
@property (nonatomic, strong) NSArray *detailTextArray;//数据注释数组
-(void)strokePath;
@end
@interface PieChartView ()
@property (nonatomic, strong) NSArray *proportionArray;// 存储各部分的比例数据
@property (nonatomic, strong) NSString *total;// 数据总额
@end

@implementation PieChartView

// 计算数据总和以及各部分的比例
-(void)handleData {
    self.total = [NSString stringWithFormat:@"%.2f", [[self.dataArray valueForKeyPath:@"@sum.floatValue"] floatValue]];
    NSMutableArray *tempArray = [NSMutableArray array];
    for (NSNumber *number in self.dataArray) {
        CGFloat numberProportion = [number floatValue]/[self.total floatValue];
        [tempArray addObject:[NSNumber numberWithFloat:numberProportion]];
    }
    self.proportionArray = [NSArray arrayWithArray:tempArray];
}

-(void)strokePath {
    [self handleData];
    CGFloat startAngle = -M_PI_2;// 开始的角度
    CGFloat endAngle = -M_PI_2;// 结束的角度
    CGFloat radius = self.frame.size.width/5;// 半径的最大值
    CGPoint centerPoint = CGPointMake(self.frame.size.width/2 , self.frame.size.height/2);// 中心点坐标

    //用来保存每段弧线夹角的中间角度和弧线中间点
    NSMutableArray *pointArray = [[NSMutableArray alloc] init];
    NSMutableArray *centreAngleArray = [[NSMutableArray alloc] init];

    CGFloat current_radius = radius;// 每一部分的半径默认为最大半径
    for (int i = 0; i < self.dataArray.count; i++ ) {
        UIColor *color = self.colorArray[i];
        NSNumber *number = self.proportionArray[i];
        //计算每一部分的弧线的绝对角度和结束角度
        CGFloat absoluteAngle = [number floatValue]*M_PI*2;
        endAngle = startAngle + absoluteAngle;
        // 每一部分的半径递减
        if (i != 0) {
            current_radius = current_radius - 10;
        }
        //画弧线
        UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:centerPoint radius:current_radius startAngle:startAngle endAngle:endAngle clockwise:**YES**];
        [path addLineToPoint:centerPoint];
        CAShapeLayer *pieLayer = [CAShapeLayer layer];
        [self.layer addSublayer:pieLayer];
        pieLayer.fillColor = color.CGColor;
        pieLayer.strokeColor = color.CGColor;
        pieLayer.path = path.CGPath;
        //计算每一部分的弧线的中间角度和弧线中间点,并保存
        CGFloat centreAngle = (startAngle + endAngle)/2;
        CGPoint centrePoint = CGPointMake(centerPoint.x + current_radius*cos(centreAngle), centerPoint.y + current_radius*sin(centreAngle));
        [centreAngleArray addObject:[NSNumber numberWithFloat:centreAngle]];
        [pointArray addObject:[NSValue valueWithCGPoint:centrePoint]];
        //记录该弧线的结束角度,它是下一个弧线的开始角度
        startAngle = endAngle;
    }
    // 添加指引线
    [self drawLineWithPointArray:pointArray centerArray:centreAngleArray];
}

// 添加指引线方法: pointArray 指引线在饼图扇区的起点  centerArray 起点的角度
-(void)drawLineWithPointArray:(NSArray *)pointArray centerArray:(NSArray *)centerArray {
    //记录每一个detail文字的frame
    CGRect rect = CGRectZero;
    CGFloat width = self.bounds.size.width*0.5;// 父视图的横向中间值
    for (int i = 0; i < pointArray.count; i++) {
        UIColor *color = self.colorArray[i];// 获取色值
        //指引线起点和角度
        CGPoint startPoint = [pointArray[i] CGPointValue];
        CGFloat radianCenter = [centerArray[i] floatValue];
        // 计算各部分数组占比,以及文字
        NSString *proportionStr = [NSString stringWithFormat:@"%.2f%%", [self.proportionArray[i] doubleValue]*100];
        NSString *text = [NSString stringWithFormat:@"%@\n%@", self.detailTextArray[i], proportionStr];
        //指引线转折点
        CGFloat turnPointTmp = 5 + i*5;
        CGFloat turnPoint_X = startPoint.x + turnPointTmp *cos(radianCenter);
        CGFloat turnPoint_Y = startPoint.y + turnPointTmp *sin(radianCenter);
        //指引线终点
        CGFloat endPoint_X = 0;
        CGFloat endPoint_Y = 0;
        //文字的frame数据
        CGFloat titleWidth = 60;
        CGFloat titleHeight = 30;
        CGFloat title_X = 0;
        CGFloat title_Y = turnPoint_Y;
        // 文字的属性
        NSMutableParagraphStyle *paragraph = [[NSMutableParagraphStyle alloc] init];
        paragraph.alignment = NSTextAlignmentLeft;
        // 每一部分弧线的中间点在左边或右边,文字和指引线结束点不同
        if (startPoint.x <= width) {// 左边
            endPoint_X = titleWidth + 15;
            endPoint_Y = turnPoint_Y;
            title_X = endPoint_X - titleWidth -7;
            paragraph.alignment = NSTextAlignmentRight;
        } else {//在右边
            endPoint_X = width*2 - (titleWidth + 15);
            endPoint_Y = turnPoint_Y;
            title_X = endPoint_X + 7;
        }
        // 文字的左边 y 值
        title_Y = endPoint_Y - titleHeight/2;
        
        // 修正 文字 可能重叠的情况
        //排除右边从上到下第一个(i == 0)和左边从下到上第一个(startPoint.x <= width && CGRectGetMidX(rect)> width)
        if (i != 0 && !(startPoint.x <= width && CGRectGetMidX(rect)> width)) {
            CGRect rect1 = CGRectMake(title_X, title_Y, titleWidth, titleHeight);
            CGFloat margin = 0;
            if (CGRectIntersectsRect(rect, rect1)) {
                /**两个面积是否重叠,有三种情况 1,压在上面  2、压在下面  3、包含*/
                if (CGRectContainsRect(rect, rect1)) {//包含
                    if (startPoint.x <= width) {//左边
                        margin = CGRectGetMaxY(rect1) -rect.origin.y;
                        endPoint_Y -= margin;
                    } else {//右边
                        margin = CGRectGetMaxY(rect) -rect1.origin.y;
                        endPoint_Y += margin;
                    }
                } else {//相交
                    if (CGRectGetMaxY(rect1) > rect.origin.y && rect1.origin.y < rect.origin.y) {//rect1下半部分 与rect上半部分重叠
                        if (startPoint.x <= width) {//左边
                            margin = CGRectGetMaxY(rect1) - rect.origin.y;
                            endPoint_Y -= margin;
                        } else {//右边
                            margin = CGRectGetMaxY(rect) -rect1.origin.y;
                            endPoint_Y += margin;
                        }
                    } else if (rect1.origin.y < CGRectGetMaxY(rect) && CGRectGetMaxY(rect1) > CGRectGetMaxY(rect)) {//rect1上半部分 与rect下半部分重叠
                        if (startPoint.x <= width) {//左边
                            margin = CGRectGetMaxY(rect1) - rect.origin.y;
                            endPoint_Y -= margin;
                        } else {//右边
                            margin = CGRectGetMaxY(rect) -rect1.origin.y;
                            endPoint_Y += margin;
                        }
                    }
                }
            } else {//相离
                if (startPoint.x <= width) {//左边
                    if (rect1.origin.y > CGRectGetMaxY(rect)) {//rect1靠下,rect靠上
                        margin = CGRectGetMaxY(rect1) - rect.origin.y;
                        endPoint_Y -= margin;
                    }
                } else {//右边
                    if (CGRectGetMaxY(rect1) < rect.origin.y) {//rect靠下,rect1靠上
                        margin = CGRectGetMaxY(rect) -rect1.origin.y;
                        endPoint_Y += margin;
                    }
                }
            }
            /**超出页面上下的情况 */
            if (startPoint.x <= width) {//左边
                if (endPoint_Y <= 0) {//
                    endPoint_Y = 0;
                    endPoint_X = CGRectGetMaxX(rect);
                }
            } else {//右边
                if (endPoint_Y+titleHeight >= **self**.frame.size.height) {
                    endPoint_Y = **self**.frame.size.height - titleHeight;
                    endPoint_X = rect.origin.x-titleWidth;
                }
            }
            title_Y = endPoint_Y - titleHeight/2;

            rect = CGRectMake(title_X, title_Y, titleWidth, titleHeight);
        } else {
            rect = CGRectMake(title_X, title_Y, titleWidth, titleHeight);
        }
        //画指引线
        UIBezierPath *path = [UIBezierPath bezierPath];
        [path moveToPoint:CGPointMake(endPoint_X, endPoint_Y)];
        [path addLineToPoint:CGPointMake(turnPoint_X, turnPoint_Y)];
        [path addLineToPoint:startPoint];

        CAShapeLayer *pieLayer = [CAShapeLayer layer];
        [self.layer addSublayer:pieLayer];
        pieLayer.fillColor = [UIColor clearColor].CGColor;
        pieLayer.strokeColor = color.CGColor;
        pieLayer.lineWidth = 1;
        pieLayer.path = path.CGPath;
        //小圆点
        CGFloat dotRadius = 3;
        UIView *dotView = [[UIView alloc] initWithFrame:CGRectMake(endPoint_X-dotRadius, endPoint_Y-dotRadius, dotRadius*2, dotRadius*2)];
        dotView.backgroundColor = color;
        dotView.layer.cornerRadius = dotRadius;
        dotView.layer.masksToBounds = **YES**;
        [self addSubview:dotView];
        //画指示文字
        NSDictionary *attributes = @{NSFontAttributeName:[UIFont systemFontOfSize:12],
                                     NSForegroundColorAttributeName: color,
                                     NSParagraphStyleAttributeName: paragraph};
        UILabel *detailLabel = [[UILabel alloc] initWithFrame:CGRectMake(title_X, title_Y, titleWidth, titleHeight)];
        detailLabel.attributedText = [[NSAttributedString alloc] initWithString:text attributes:attributes];
        detailLabel.numberOfLines = 2;
        [self addSubview:detailLabel];
    }
}