之前项目中需要用到饼状图来统计数据,我在Github上找到DVPieChart,因为它在样式上与我们UI的设计图差异不大。拿来改改样式,按需要传入数据就解决了问题。后来,需要统计的数据条数变得更多了,这时发现DVPieChart的view上展示不了太多标签,很多指示线还会交叉。后面在Github没重新找到更合适的饼状图,就只有自己提笔按照需求重写了一个状图。
绘制逻辑会也不复杂。
1、先画好饼状图(需要环状图的话放个白色的圆覆盖在中心就行了);
2、再去view的四周计算文本的放置区域;
3、写个算法配对距离每个饼状图区域最合适的文本区域;
4、绘制饼状图各区域到最合适的文本区域的指示线和文本区域的文字;
示意图:
构建数据初始化饼状图
#import "ViewController.h"
#import "DVPieChart.h"
#import "DVFoodPieModel.h"
#import "UIColor+HexColor.h"
#define HexColor(hexString) [UIColor colorWithHexString:(hexString)]
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 图1
DVPieChart *chart1 = [[DVPieChart alloc] initWithFrame:CGRectMake(20, 44, [UIScreen mainScreen].bounds.size.width-40, 320)];
chart1.useTopZone = YES;
chart1.useBottomZone = YES;
chart1.useTextSize = CGSizeMake(60, 30);
chart1.showTextBorder = YES;
chart1.verticalLineHeight = 0;
[self.view addSubview:chart1];
NSMutableArray *tempArray = [NSMutableArray new];
for (int i = 0 ; i < 20 ; i ++) {
[tempArray addObject:@{@"title":[NSString stringWithFormat:@"数据%d",i], @"value":[NSString stringWithFormat:@"%d",200+600*(i%7)]}];
}
[self setupChartView:chart1 ringsArray:tempArray];
// 图2
DVPieChart *chart2 = [[DVPieChart alloc] initWithFrame:CGRectMake(20, 380, [UIScreen mainScreen].bounds.size.width-40, 320)];
[self.view addSubview:chart2];
NSMutableArray *tempArray2 = [NSMutableArray new];
for (int i = 0 ; i < 20 ; i ++) {
[tempArray2 addObject:@{@"title":[NSString stringWithFormat:@"数据%d",i], @"value":[NSString stringWithFormat:@"%d",200+600*(i%7)]}];
}
[self setupChartView:chart2 ringsArray:tempArray2];
}
- (void)setupChartView:(DVPieChart *)chartView ringsArray:(NSMutableArray *)ringsArray {
DVPieChart *tempChartView = chartView;
NSMutableArray * dataArr = [NSMutableArray array];
NSMutableArray * values = [NSMutableArray array];
for (NSDictionary * dict in ringsArray) {
[values addObject:[NSNumber numberWithDouble:[dict[@"value"] doubleValue]]];
}
NSNumber *sum = [values valueForKeyPath:@"@sum.self"];
for (NSDictionary * dict in ringsArray) {
DVFoodPieModel * newModel = [[DVFoodPieModel alloc] init];
newModel.value = [dict[@"value"] doubleValue];
newModel.name = dict[@"title"];
CGFloat value = 0;
if ([sum doubleValue] != 0) {
value = [dict[@"value"] doubleValue]/[sum doubleValue];
}
newModel.rate = value;
[values addObject:[NSNumber numberWithDouble:[dict[@"value"] doubleValue]]];
[dataArr addObject:newModel];
}
tempChartView.dataArray = dataArr;
if ([sum doubleValue] > 0) {//有数据才绘制
tempChartView.hidden = NO;
[tempChartView draw];
} else {
tempChartView.hidden = YES;
}
NSArray * clArr = @[HexColor(@"#E76344"),
HexColor(@"#55A3F4"),
HexColor(@"#F6BA57"),
HexColor(@"#71D6FF"),
HexColor(@"#E371FF"),
HexColor(@"#8365E2"),
HexColor(@"#46D295"),
HexColor(@"#A8E12A"),
HexColor(@"#5B48A7"),
HexColor(@"#FF0099"),
HexColor(@"#F79709"),
HexColor(@"#3DEE11"),
HexColor(@"#0938F7"),
HexColor(@"#9709F7"),
HexColor(@"#EE113D"),
HexColor(@"#CC5233"),
HexColor(@"#A25E87"),
HexColor(@"#B39E4D"),
HexColor(@"#666699"),
HexColor(@"#A9C43C"),
HexColor(@"#BE3085"),
HexColor(@"#7F796F"),
HexColor(@"#386AB7"),
HexColor(@"#08E78D")
];
tempChartView.COLOR_ARRAY = clArr;
}
@end
DVPieChart(饼状图绘制)
#import <UIKit/UIKit.h>
@interface DVPieChart : UIView
/// 展示出文本区域边框
@property (assign, nonatomic) BOOL showTextBorder;
/// 是否使用上方区域 展示标签,默认不使用此区域
@property (assign, nonatomic) BOOL useTopZone;
/// 是否使用下方区域 展示标签,默认不使用此区域
@property (assign, nonatomic) BOOL useBottomZone;
/// 文本区域的大小。 默认CGSizeMake(80, 30)
@property (assign, nonatomic) CGSize useTextSize;
/// 左右指引线的横线宽度. 默认20
@property (assign, nonatomic) float horizontalLineWidth;
/// 上下指引线的竖线高度. 默认20
@property (assign, nonatomic) float verticalLineHeight;
/// 数据数组
@property (strong, nonatomic) NSArray * COLOR_ARRAY;
/// 数据数组
@property (strong, nonatomic) NSArray *dataArray;
/// 标题
@property (copy, nonatomic) NSString *title;
/// 绘制方法
- (void)draw;
@end
#import "DVPieChart.h"
#import "DVFoodPieModel.h"
#import "DVPieCenterView.h"
#import "DVPieLocationModel.h"
@interface DVPieChart ()
@property (nonatomic, strong) NSMutableArray *modelArray;
@property (nonatomic, strong) NSMutableArray *colorArray;
@property (nonatomic, strong) NSMutableArray *allLocationArray;
@property (nonatomic, strong) NSMutableArray *pointArray;
@property (nonatomic, strong) NSMutableArray *centerArray;
@property (nonatomic, assign) NSInteger beforeIndex; // 前一个点所在区域的索引
@property (nonatomic, assign) NSInteger firstZoneIndex; // 分配的第一个区域的索引
@end
@implementation DVPieChart
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
self.backgroundColor = [UIColor whiteColor];
_firstZoneIndex = -1;
_beforeIndex = -1;
_useTextSize = CGSizeMake(80, 30);
_horizontalLineWidth = 20;
_verticalLineHeight = 20;
}
return self;
}
- (void)draw {
[self.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
[self setNeedsDisplay];
}
- (BOOL)judgeIsMinScreen {
BOOL result = NO;
if ([UIScreen mainScreen].bounds.size.width <= 375) {
result = YES;
}
return result;
}
- (void)drawRect:(CGRect)rect {
float CHART_MARGIN = 90;
if ([self judgeIsMinScreen]) {
CHART_MARGIN = 100;
}
CGFloat useStart = 0;
CGFloat min = self.bounds.size.width > self.bounds.size.height ? self.bounds.size.height : self.bounds.size.width;
CGPoint center = CGPointMake(self.bounds.size.width * 0.5, self.bounds.size.height * 0.5);
CGFloat radius = min * 0.5 - CHART_MARGIN;
CGFloat start = useStart;
CGFloat angle = 0;
CGFloat end = start;
if (self.dataArray.count == 0) {
end = start + M_PI * 2;
UIColor *color = _COLOR_ARRAY.firstObject;
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:start endAngle:end clockwise:true];
[color set];
// 添加一根线到圆心
[path addLineToPoint:center];
[path fill];
} else {
NSMutableArray *pointArray = [NSMutableArray array];
NSMutableArray *centerArray = [NSMutableArray array];
self.modelArray = [NSMutableArray array];
self.colorArray = [NSMutableArray array];
for (int i = 0; i < self.dataArray.count; i++) {
DVFoodPieModel *model = self.dataArray[i];
CGFloat percent = model.rate;//NaN
UIColor *color = _COLOR_ARRAY[i%_COLOR_ARRAY.count];
start = end;
angle = percent * M_PI * 2;
end = start + angle;
// 获取弧度的中心角度
CGFloat radianCenter = (start + end) * 0.5;
// 获取指引线的起点
CGFloat lineStartX = self.frame.size.width * 0.5 + radius * cos(radianCenter);
CGFloat lineStartY = self.frame.size.height * 0.5 + radius * sin(radianCenter);
CGPoint point = CGPointMake(lineStartX, lineStartY);
[pointArray addObject:[NSValue valueWithCGPoint:point]];
[centerArray addObject:[NSNumber numberWithFloat:radianCenter]];
[self.modelArray addObject:model];
[self.colorArray addObject:color];
}
// 处理数据顺序
// 通过pointArray绘制指引线
[self drawLineWithPointArray:pointArray centerArray:centerArray];
start = useStart;
angle = 0;
end = start;
for (int i = 0; i < self.dataArray.count; i++) {
DVFoodPieModel *model = self.dataArray[i];
CGFloat percent = model.rate;//NaN
UIColor *color = _COLOR_ARRAY[i%_COLOR_ARRAY.count];
start = end;
angle = percent * M_PI * 2;
end = start + angle;
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:start endAngle:end clockwise:true];
[color set];
//添加一根线到圆心
[path addLineToPoint:center];
[path fill];
}
}
// 在中心添加label
DVPieCenterView *centerView = [[DVPieCenterView alloc] init];
centerView.frame = CGRectMake(0, 0, radius * 2 - 24, radius * 2 - 24);
CGRect frame = centerView.frame;
frame.origin = CGPointMake(self.frame.size.width * 0.5 - frame.size.width * 0.5, self.frame.size.height * 0.5 - frame.size.width * 0.5);
centerView.frame = frame;
[self addSubview:centerView];
}
// 计算和约定所有的内容位置
- (void)textAllLocation {
if (!_allLocationArray) {
_allLocationArray = [NSMutableArray new];
}
[_allLocationArray removeAllObjects];
CGRect selfRect = self.frame;
// 文本区域大小
CGSize textSize = self.useTextSize;
// 左右的横线长度
float levelDistance = self.horizontalLineWidth;
// 上下的竖线长度
float topLevelDistance = self.verticalLineHeight;
float baseX = 0;;
float changex = baseX;
NSInteger p = (NSInteger)((selfRect.size.width - 2*textSize.width - 2*baseX) / textSize.width);
changex = (selfRect.size.width - p*textSize.width)/2.0;
float baseY = 10;
float changeY = baseY;
NSInteger s = (NSInteger)selfRect.size.height % (NSInteger)textSize.height;
if (s < baseX*2) {
s = s + textSize.height;
}
changeY = s/2.0;
// 0:右侧
NSMutableArray *tempRightArr = [NSMutableArray new];
for (int i = 0 ; i < 100 ; i ++) {
DVPieLocationModel *loc = [DVPieLocationModel new];
CGRect newRect = CGRectMake(selfRect.size.width - baseX - textSize.width, changeY + i*textSize.height, textSize.width, textSize.height);
loc.locationRect = newRect;
loc.inPoint = CGPointMake(CGRectGetMinX(newRect), CGRectGetMinY(newRect)+textSize.height/2.0);
loc.levelPoint = CGPointMake(loc.inPoint.x-levelDistance, loc.inPoint.y);
loc.direction = 0;
if (CGRectGetMaxY(newRect) > selfRect.size.height - baseY) {
break;
}
[tempRightArr addObject:loc];
}
[_allLocationArray addObjectsFromArray:tempRightArr];
// 1:下侧
if (_useBottomZone) {
NSMutableArray *tempBottomArr = [NSMutableArray new];
for (int i = 0 ; i < 10 ; i ++) {
DVPieLocationModel *loc = [DVPieLocationModel new];
CGRect newRect = CGRectMake(changex + i*textSize.width, selfRect.size.height - textSize.height, textSize.width, textSize.height);
loc.locationRect = newRect;
loc.inPoint = CGPointMake(CGRectGetMinX(newRect) + textSize.width/2.0, CGRectGetMinY(newRect));
loc.levelPoint = CGPointMake(loc.inPoint.x, loc.inPoint.y-topLevelDistance);
loc.direction = 1;
if (CGRectGetMaxX(newRect) > selfRect.size.width - changex) {
break;
}
[tempBottomArr addObject:loc];
}
tempBottomArr = (NSMutableArray *)[[tempBottomArr reverseObjectEnumerator] allObjects];
[_allLocationArray addObjectsFromArray:tempBottomArr];
}
// 2:左侧
NSMutableArray *tempLeftArr = [NSMutableArray new];
for (int i = 0 ; i < 100 ; i ++) {
DVPieLocationModel *loc = [DVPieLocationModel new];
CGRect newRect = CGRectMake(baseX, changeY + i*textSize.height, textSize.width, textSize.height);
loc.locationRect = newRect;
loc.inPoint = CGPointMake(CGRectGetMaxX(newRect), CGRectGetMinY(newRect)+textSize.height/2.0);
loc.levelPoint = CGPointMake(loc.inPoint.x+levelDistance, loc.inPoint.y);
loc.direction = 2;
if (CGRectGetMaxY(newRect) > selfRect.size.height - baseY) {
break;
}
[tempLeftArr addObject:loc];
}
tempLeftArr = (NSMutableArray *)[[tempLeftArr reverseObjectEnumerator] allObjects];
[_allLocationArray addObjectsFromArray:tempLeftArr];
// 3:上侧
if (_useTopZone) {
NSMutableArray *tempTopArr = [NSMutableArray new];
for (int i = 0 ; i < 10 ; i ++) {
DVPieLocationModel *loc = [DVPieLocationModel new];
CGRect newRect = CGRectMake(changex + i*textSize.width, 0, textSize.width, textSize.height);
loc.locationRect = newRect;
loc.inPoint = CGPointMake(CGRectGetMinX(newRect) + textSize.width/2.0, CGRectGetMaxY(newRect));
loc.levelPoint = CGPointMake(loc.inPoint.x, loc.inPoint.y+topLevelDistance);
loc.direction = 3;
if (CGRectGetMaxX(newRect) > selfRect.size.width - changex) {
break;
}
[tempTopArr addObject:loc];
}
[_allLocationArray addObjectsFromArray:tempTopArr];
}
/// 文本框提示
if (self.showTextBorder) {
for (DVPieLocationModel *loc in _allLocationArray) {
UIView *theView = [UIView new];
theView.frame = loc.locationRect;
[theView.layer setBorderColor:[[UIColor blueColor] CGColor]];
[theView.layer setBorderWidth:1];
[self addSubview:theView];
UIView *tempView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 5, 5)];
tempView.center = loc.inPoint;
tempView.backgroundColor = [UIColor redColor];
[self addSubview:tempView];
}
}
}
// 根据一个点获取距离它最近的内容区
- (DVPieLocationModel *)getZonePointIndex:(NSInteger)index {
// 剩余未分配点的个数。
NSInteger dataCount = _pointArray.count;
if (dataCount > _allLocationArray.count) {
dataCount = _allLocationArray.count;
}
NSInteger residueCount = dataCount - (index+1);
// 当前要分配的点
CGPoint point = [self getGuidePointWithIndex:index];
DVPieLocationModel *rModel;
CGFloat minDistance = MAXFLOAT;
NSInteger selectIndex = -1;
for (int i = 0 ; i < _allLocationArray.count ; i ++) {
// 后面的不允许连接前面的几个点,这样会导致线条交叉,去掉这几个点后后面的点就更近了
if (_beforeIndex != -1) {
NSInteger dev = 0;
if (_beforeIndex > i) {
dev = -(_beforeIndex-(i+1));
if (dev <= 0 && dev > -8) {
continue;
}
} else {
dev = -(_allLocationArray.count - (i+1) + _beforeIndex);
if (dev <= 0 && dev > -8) {
continue;
}
}
}
// 第一个分配区域逆时针到目前分配的区域不能小于剩余分配点的个数
if (_firstZoneIndex != -1) {
NSInteger dev = 0;
if (_firstZoneIndex < i) {
dev = _allLocationArray.count - (i+1) + _firstZoneIndex;
} else {
dev = _firstZoneIndex - (i+1);
}
if (dev < residueCount) {
continue;
}
}
// 已使用的点,不再参与计算
DVPieLocationModel *loc = [_allLocationArray objectAtIndex:i];
if (!loc.isUse) {
CGFloat distance1 = [self jsDistanceWithPointOne:point PointTwo:loc.levelPoint];
CGFloat distance2 = [self jsDistanceWithPointOne:loc.levelPoint PointTwo:loc.inPoint];
CGFloat distance = distance1 + distance2;
if (loc.direction == 1 || loc.direction == 3) {
distance = distance + 32; // 上、下 增加计算距离
}
if (distance < minDistance) {
minDistance = distance;
rModel = loc;
selectIndex = i;
}
}
}
if (selectIndex != -1) {
if (_beforeIndex == -1) {
_firstZoneIndex = selectIndex;
}
_beforeIndex = selectIndex;
}
return rModel;
}
// 计算两点之间的距离
- (float)jsDistanceWithPointOne:(CGPoint)pointOne PointTwo:(CGPoint)pointTwo {
CGFloat xDist = (pointOne.x - pointTwo.x);
CGFloat yDist = (pointOne.y - pointTwo.y);
CGFloat distance = sqrt((xDist * xDist) + (yDist * yDist));
return distance;
}
- (void)drawLineWithPointArray:(NSMutableArray *)pointArray centerArray:(NSMutableArray *)centerArray {
_pointArray = pointArray;
_centerArray = centerArray;
[self textAllLocation];
for (int i = 0; i < pointArray.count; i++) {
// 颜色(绘制数据时要用)
UIColor *color = self.colorArray[i];
// 模型数据(绘制数据时要用)
DVFoodPieModel *model = self.modelArray[i];
// 模型的数据
NSString *number = [NSString stringWithFormat:@"%.2f%%", model.rate * 100];
NSString *name = [NSString stringWithFormat:@"%@",model.name];
// 指引线终点的位置(x, y)
CGPoint startPoint = [self getGuidePointWithIndex:i];
CGFloat startX = startPoint.x;
CGFloat startY = startPoint.y;
// 文本段落属性(绘制文字和数字时需要)
NSMutableParagraphStyle * paragraph = [[NSMutableParagraphStyle alloc]init];
// 文字靠右
paragraph.alignment = NSTextAlignmentRight;
// 指引线起点(x, y)
DVPieLocationModel *loModel = [self getZonePointIndex:i];
if (!loModel) {
return;
}
loModel.isUse = YES;
CGFloat endX = loModel.inPoint.x;
CGFloat endY = loModel.inPoint.y;
// 绘制文字和数字时的起始位置(x, y)与上面的合并起来就是frame
CGFloat numberX;
CGFloat numberY = 0.0;
// 绘制文字和数字时,所占的size(width和height)
// width使用lineWidth更好,我这么写固定值是为了达到产品要求
CGFloat numberWidth = 80.f;
CGFloat numberHeight = 15.f;
/// 方向
if (loModel.direction == 0) { // 右侧
paragraph.alignment = NSTextAlignmentLeft;
numberX = endX;
numberY = endY - numberHeight + 2;
} else if (loModel.direction == 1) { // 下侧
paragraph.alignment = NSTextAlignmentLeft;
numberX = endX - numberWidth/4;
// numberX = endX - 0;
numberY = endY;
} else if (loModel.direction == 2) { // 左侧
paragraph.alignment = NSTextAlignmentRight;
numberX = endX - numberWidth;
numberY = endY - numberHeight;
} else { // 上侧
paragraph.alignment = NSTextAlignmentLeft;
numberX = endX - numberWidth/4;
// numberX = endX - 0;
numberY = endY - numberHeight - 5;
}
//1.获取上下文
CGContextRef ctx = UIGraphicsGetCurrentContext();
//2.绘制路径
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(endX, endY)];
[path addLineToPoint:CGPointMake(loModel.levelPoint.x, loModel.levelPoint.y)];
[path addLineToPoint:CGPointMake(startX, startY)];
CGContextSetLineWidth(ctx, 0.5);
//设置颜色
[color set];
//3.把绘制的内容添加到上下文当中
CGContextAddPath(ctx, path.CGPath);
//4.把上下文的内容显示到View上(渲染到View的layer)(stroke fill)
CGContextStrokePath(ctx);
// 在终点处添加点(小圆点)
// movePoint,让转折线指向小圆点中心
UIView *view = [[UIView alloc] init];
view.backgroundColor = color;
view.alpha = 0.9;
[self addSubview:view];
CGRect viewRect = view.frame;
viewRect.size = CGSizeMake(4, 4);
viewRect.origin = CGPointMake(startX-2, startY-2);
view.frame = viewRect;
view.layer.cornerRadius = 2;
view.layer.masksToBounds = true;
// 指引线上面的数字
[name drawInRect:CGRectMake(numberX, numberY + 8, numberWidth, numberHeight) withAttributes:@{NSFontAttributeName:[UIFont systemFontOfSize:11], NSForegroundColorAttributeName:color,NSParagraphStyleAttributeName:paragraph}];
[number drawInRect:CGRectMake(numberX, numberY, numberWidth, numberHeight) withAttributes:@{NSFontAttributeName:[UIFont systemFontOfSize:9], NSForegroundColorAttributeName:color,NSParagraphStyleAttributeName:paragraph}];
}
}
- (CGPoint)getGuidePointWithIndex:(NSInteger)index {
// 每个圆弧中心店的位置
CGPoint point = [_pointArray[index] CGPointValue];
// 每个圆弧中心点的角度
CGFloat radianCenter = [_centerArray[index] floatValue];
// 指引线终点的位置(x, y)
CGFloat startX = point.x + 5 * cos(radianCenter);
CGFloat startY = point.y + 5 * sin(radianCenter);
return CGPointMake(startX, startY);
}
@end