源码解析--PNChart
前言:
PNChart
是一个简单漂亮的iOS图表库,在github上
面获得了8000多个star,建议先下载这个库配合本文的阅读。它支持以下图形的绘制:
- PNCircleChart(环形图)
- PNLineChart(折线图)
- PNPieChart(饼图)
- PNBarChart(柱状图)
- PNRadarChart(雷达图)
- PNScatterChart(散点图)
层次结构为:
层次结构图.png
我们开始吧:
PNCircleChart(环形图)
PNCircleChart
和其他图不一样,它是直接继承UIView
。为了方便讲解,我故意添加了背景色和渐变色。如图:
PNCircleChart
主要组成部分:
@property (strong, nonatomic) UICountingLabel *countingLabel;//显示百分比文本
@property (nonatomic) CAShapeLayer *circle;//蓝色部分,部分被渐变绿色覆盖
@property (nonatomic) CAShapeLayer *gradientMask;//上图深绿色的部分
@property (nonatomic) CAShapeLayer *circleBackground;//上图灰色部分
PNCircleChart结构图.png
1.拿到图形的路径:
UIBezierPath *circlePath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.frame.size.width/2.0f, self.frame.size.height/2.0f)
radius:(self.frame.size.height * 0.5) - ([_lineWidth floatValue]/2.0f)
startAngle:DEGREES_TO_RADIANS(startAngle)
endAngle:DEGREES_TO_RADIANS(endAngle)
clockwise:clockwise];
2.添加渐变颜色
// Add gradient
self.gradientMask = [CAShapeLayer layer];
self.gradientMask.fillColor = [[UIColor clearColor] CGColor];
self.gradientMask.strokeColor = [[UIColor blackColor] CGColor];
self.gradientMask.lineWidth = _circle.lineWidth;
self.gradientMask.lineCap = kCALineCapRound;
CGRect gradientFrame = CGRectMake(0, 0, 2*self.bounds.size.width, 2*self.bounds.size.height);
self.gradientMask.frame = gradientFrame;
self.gradientMask.path = _circle.path;
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
gradientLayer.startPoint = CGPointMake(0.5,1.0);
gradientLayer.endPoint = CGPointMake(0.5,0.0);
gradientLayer.frame = gradientFrame;
UIColor *endColor = (_strokeColor ? _strokeColor : [UIColor greenColor]);
NSArray *colors = @[
(id)endColor.CGColor,
(id)_strokeColorGradientStart.CGColor
];
gradientLayer.colors = colors;
//如果不添加,你会发现self.gradientMask 添加在self上了,你可以试试
[gradientLayer setMask:self.gradientMask];
[_circle addSublayer:gradientLayer];
3.UICountingLabel
类主要是来实现数字平滑变化的动画。利用CABasicAnimation来实现layer层的动画。动画的相关内容可以参考这里
PNLineChart(折线图)
折线图.png1.减去左右黄色边距区域,拿到横轴作图区域
_chartCavanWidth = self.frame.size.width - _chartMarginLeft - _chartMarginRight;
2.红色区域根据数组确定点横坐标,以及布局相关label。
[self.lineChart setXLabels:@[@"SEP 1",@"SEP 2",@"SEP 3",@"SEP 4",@"SEP 5",@"SEP 6",@"SEP 7"]];
3.拿到纵轴作图区域,布局相关label。
_chartCavanHeight = self.frame.size.height - _chartMarginBottom - _chartMarginTop;
[self.lineChart setYLabels:@[
@"0 min",
@"50 min",
@"100 min",
@"150 min",
@"200 min",
@"250 min",
@"300 min",
]
];
4.根据提供的点的大小与刚刚计算的纵轴和横轴的值,计算出点具体的frame
。然后根据点与点计算点与点的路径。存储下来。并利用UIBezierPath
和CAShapeLayer
作动画。
self.lineChart.chartData = @[data01, data02];//在setter方法里面去做计算路径的操作
[self.lineChart strokeChart]; //画图
//计算x轴的点
int x = i * _xLabelWidth + _chartMarginLeft + _xLabelWidth / 2.0;
//计算y轴的点
int y = _chartCavanHeight - (innerGrade * _chartCavanHeight) - (_yLabelHeight / 2) + _chartMarginTop;
5.PNLineChartData
是关于PNLineChart
的一个非常重要的类,它为PNLineChart提供线条相关的颜色,文本字体,点样式信息,比如:
typedef NS_ENUM(NSUInteger, PNLineChartPointStyle) {
PNLineChartPointStyleNone = 0, //无
PNLineChartPointStyleCircle = 1,//圆点
PNLineChartPointStyleSquare = 3,//正方形点
PNLineChartPointStyleTriangle = 4//三角形点
};
@property (nonatomic) BOOL showPointLabel; //当PNLineChartPointStyle不为PNLineChartPointStyleNone样式时,决定是否在点上面显示点信息的文本。比如上图绿色的区域。
PNBarChart(柱状图)
PNBarChart
柱状图和PNLineChart
是极其相似的,只不过在确定坐标系后利用去布局PNBar柱对象,PNBar对象负责每个柱对象的样式,颜色和动画。
动画的实现:
-(void)addAnimationIfNeededWithProgressLine:(UIBezierPath *)progressline
{
if (self.displayAnimated) {
CABasicAnimation *pathAnimation = nil;
if (_grade) {
pathAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
pathAnimation.fromValue = (id)_chartLine.path;
pathAnimation.toValue = (id)[progressline CGPath];
pathAnimation.duration = 0.5f;
pathAnimation.autoreverses = NO;
pathAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[_chartLine addAnimation:pathAnimation forKey:@"animationKey"];
}
else {
pathAnimation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
pathAnimation.duration = 1.0;
pathAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
pathAnimation.fromValue = @0.0f;
pathAnimation.toValue = @1.0f;
[_chartLine addAnimation:pathAnimation forKey:@"strokeEndAnimation"];
}
[self.gradientMask addAnimation:pathAnimation forKey:@"animationKey"];
}
}
PNPieChart(饼图)
PNPieChart
柱状可以显示当前模块所占百分比,也可以选择选择本身的值。如图(百分比):
1.
PNPieChart
初始化时会根据比较宽高德大小来以较小的设置直径,以免超出页面。
CGFloat minimal = (CGRectGetWidth(self.bounds) < CGRectGetHeight(self.bounds)) ? CGRectGetWidth(self.bounds) : CGRectGetHeight(self.bounds); //利用MIN宏会比三目运算更可读
2.PNPieChartDataItem类
提供包装初始化数据,根据数据提供的itemValue
计算出各个item
所占比例。我们提供的初始化数据为:
NSArray *items = @[[PNPieChartDataItem dataItemWithValue:10 color:PNLightGreen],
[PNPieChartDataItem dataItemWithValue:20 color:PNFreshGreen description:@"WWDC"],
[PNPieChartDataItem dataItemWithValue:40 color:PNDeepGreen description:@"GOOG I/O"],
[PNPieChartDataItem dataItemWithValue:30 color:PNMauve description:@"ATR"],
];
3.计算半径,拿到准备动画绘制路径。
//计算半径以及借下来的lineWidth
self.outerCircleRadius = minimal / 2;
self.innerCircleRadius = minimal / 6;
CGFloat radius = _innerCircleRadius + (_outerCircleRadius - _innerCircleRadius) / 2;
CGFloat borderWidth = _outerCircleRadius - _innerCircleRadius;
- (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));
//从坐标轴-90°出发,拿到路径。
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;
//根据strokeStart,strokeEnd绘制每个item所占的比例
circle.strokeStart = startPercentage;
circle.strokeEnd = endPercentage;
circle.lineWidth = borderWidth;
circle.path = path.CGPath;
return circle;
}
4.添加要显示的文本
for (int i = 0; i < _items.count; i++) {
UILabel *descriptionLabel = [self descriptionLabelForItemAtIndex:i];
[_contentView addSubview:descriptionLabel];
[_descriptionLabels addObject:descriptionLabel];
}
5.点击事件
//拿到点击的坐标
CGPoint touchLocation = [touch locationInView:_contentView];
//根据点击的点的坐标位置来判断点击的哪块区域,做相应的判断
- (void)didTouchAt:(CGPoint)touchLocation
PNScatterChart(散点图)
散点图.png1.
PNScatterChart
根据分别设置X,Y坐标的最小值,和最大值,间断数来确定X,Y的坐标轴。
//比如:x的间距:(100 - 20)/(6 - 1)
[self.scatterChart setAxisXWithMinimumValue:20 andMaxValue:100 toTicks:6];
[self.scatterChart setAxisYWithMinimumValue:30 andMaxValue:50 toTicks:5];
2,然后将点绘制在坐标轴中,这个和折线图思路是一致的。
PNRadarChart(雷达图)
雷达图1.
PNRadarChartDataItem
类提供包装初始化数据,首先根据item个数决定每个item的角度。
//初始化数据
NSArray *items = @[[PNRadarChartDataItem dataItemWithValue:3 description:@"Art"],
[PNRadarChartDataItem dataItemWithValue:2 description:@"Math"],
[PNRadarChartDataItem dataItemWithValue:8 description:@"Sports"],
[PNRadarChartDataItem dataItemWithValue:5 description:@"Literature"],
[PNRadarChartDataItem dataItemWithValue:4 description:@"Other"],
];
for (int i=0;i<_chartData.count;i++) {
PNRadarChartDataItem *item = (PNRadarChartDataItem *)[_chartData objectAtIndex:i];
[descriptions addObject:item.textDescription];
[values addObject:[NSNumber numberWithFloat:item.value]];
CGFloat angleValue = (float)i/(float)[_chartData count]*2*M_PI;
[angles addObject:[NSNumber numberWithFloat:angleValue]];
}
2.拿到最大的值(我们这里是8),根据PNRadarChartLabelStyle
来计算margin
。然后计算出每小格的单位长度_lengthUnit
。然后根据angleValue
和_lengthUnit
计算出每个层5个点的坐标放在_pointsToWebArrayArray
。
//拿到最大的值
_maxValue = [self getMaxValueFromArray:values];
CGFloat margin = 0;
if (_labelStyle==PNRadarChartLabelStyleCircle) {
margin = MIN(_centerX , _centerY)*3/10;
}else if (_labelStyle==PNRadarChartLabelStyleHorizontal) {
margin = [self getMaxWidthLabelFromArray:descriptions withFontSize:_fontSize];
}
CGFloat maxLength = ceil(MIN(_centerX, _centerY) - margin);
int plotCircles = (_maxValue/_valueDivider);
if (plotCircles > MAXCIRCLE) {
NSLog(@"Circle number is higher than max");
plotCircles = MAXCIRCLE;
_valueDivider = _maxValue/plotCircles;
}
_lengthUnit = maxLength/plotCircles;
NSArray *lengthArray = [self getLengthArrayWithCircleNum:(int)plotCircles];
//get all the points and plot
for (NSNumber *lengthNumber in lengthArray) {
CGFloat length = [lengthNumber floatValue];
[_pointsToWebArrayArray addObject:[self getWebPointWithLength:length angleArray:angles]];
}
3.根据values
数组里面的itemValue值和角度来计算点的坐标放在_pointsToPlotArray
里面。
int section = 0;
for (id value in values) {
CGFloat valueFloat = [value floatValue];
if (valueFloat>_maxValue) {
NSString *reason = [NSString stringWithFormat:@"Value number is higher than max -value: %f - maxValue: %f",valueFloat,_maxValue];
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:reason userInfo:nil];
return;
}
CGFloat length = valueFloat/_maxValue*maxLength;
CGFloat angle = [[angles objectAtIndex:section] floatValue];
CGFloat x = _centerX +length*cos(angle);
CGFloat y = _centerY +length*sin(angle);
NSValue* point = [NSValue valueWithCGPoint:CGPointMake(x, y)];
[_pointsToPlotArray addObject:point];
section++;
}
4.根据最大值和角度设置lable"
[self drawLabelWithMaxLength:maxLength labelArray:descriptions angleArray:angles];
PNChartDelegate
PNGenericChart
的点击事件会通过PNChartDelegate
协议接口给暴露出来。