PNChart图表库学习笔记 - 折线图、柱形图、饼图
单位项目中需要用到图表类的功能,因此Github找了一些,以PNChart为例讲一下使用过程。
先看一下我实现的效果。
柱形图和饼图: QQ20190423-other-HD.gif
折线图
@property (nonatomic, strong) PNLineChart * lineChart;
// 初始化折线图
- (void)initializeLineChart {
_lineChart = [[PNLineChart alloc] initWithFrame:CGRectMake(0, kNavHeight+55, SCREEN_WIDTH, SCREEN_WIDTH-100)];
_lineChart.backgroundColor = [UIColor clearColor];
// 整个图表的宽度和高度,默认自动计算
// chartCavanWidth和chartCavanHeight组成图表内容的可显示区域(包含坐标轴,但不包含坐标轴的刻度值)
//_lineChart.chartCavanWidth = SCREEN_WIDTH-60;
//_lineChart.chartCavanHeight = SCREEN_WIDTH-100;
}
标准折线图
- (void)loadBasicLineChart {
[self initializeLineChart];
// 是否显示 xy 坐标轴(默认不显示)
_lineChart.showCoordinateAxis = YES;
// 坐标轴宽度
_lineChart.axisWidth = 1;
// 坐标轴颜色
_lineChart.axisColor = RGBColor(153, 153, 153);
// 设置Y轴坐标值的最大值、最小值
_lineChart.yFixedValueMax = 520;
_lineChart.yFixedValueMin = 400;
// Y轴相关设置
// 是否显示垂直于Y轴的横向虚线
_lineChart.showYGridLines = YES;
// 横向虚线颜色
_lineChart.yGridLinesColor = SeparatorLineColor_Darker;
// 轴单位
_lineChart.yUnit = @"人数";
// 刻度值内容格式(不设置或者设置为@"%1.f"为整型,@"%1.1f"为小数点后一位)
//_lineChart.yLabelFormat = @"%1.1f";
// 刻度值字体颜色
_lineChart.yLabelColor = RGBColor(102, 102, 102);
// 刻度值字体大小
_lineChart.yLabelFont = [UIFont systemFontOfSize:10];
// 设置轴数据 如果需要设置刻度值字体大小和颜色,在设置轴数据前设置,否则无效
//[_lineChart setYLabels:@[@"400", @"420", @"440", @"460", @"480", @"500"]];
// X轴相关设置
_lineChart.xUnit = @"年限";
_lineChart.xLabelColor = RGBColor(102, 102, 102);
_lineChart.xLabelFont = [UIFont systemFontOfSize:10];
// 设置x轴的数据
[_lineChart setXLabels:@[@"2010", @"2011", @"2012", @"2013", @"2014", @"2015", @"2016", @"2017", @"2018"]];
// 设置每个点的y值
// 初始化折线数据
NSArray *dataArray = @[@405, @426, @455, @461, @428, @446, @473, @496, @488];
PNLineChartData *lineData = [PNLineChartData new];
// 设置折线的颜色 & 透明度
lineData.color = PNTwitterColor;
lineData.alpha = 0.5f;
// 设置折线的宽度
lineData.lineWidth = 2.0;
// 设置折线的点数
lineData.itemCount = dataArray.count;
// 设置是否展示折点的Label
//lineData.showPointLabel = YES;
// 设置折点的大小
//lineData.inflexionPointWidth = 4.0f;
// 设置折点的文本颜色
//lineData.pointLabelColor = RGBColor(102, 102, 102);
// 设置折点的文本字体
//lineData.pointLabelFont = [UIFont systemFontOfSize:12];
// 设置折点的样式
//lineData.inflexionPointStyle = PNLineChartPointStyleCircle;
// 这个block的作用是将上面的dataArray里的每一个值传给line chart。
lineData.getData = ^(NSUInteger index) {
CGFloat yValue = [dataArray[index] floatValue];
return [PNLineChartDataItem dataItemWithY:yValue];
};
// 数据添加到图表中
_lineChart.chartData = @[lineData];
// 是否展示平滑线条(默认为NO)
//self.lineChart.showSmoothLines = YES;
//绘制图表
[_lineChart strokeChart];
//设置代理,响应点击
_lineChart.delegate = self;
// 添加至视图
[self.view addSubview:_lineChart];
}
但是运行结果后你可能会发现,图表中每一项数据与对应的X轴坐标值有或大或小的出入或者X轴坐标值不在刻度线正下方而是两个相邻刻度线的中间(具体与数据项多少有关)
result-01.png在网上看到了别人的建议和解决办法,是需要修改源码。
在PNLineChart.m文件的 setXLabels: withWidth: 方法中(大约在179行)需要进行修改:
if (_showLabel) {
for (int index = 0; index < xLabels.count; index++) {
labelText = xLabels[index];
/* 源码
NSInteger x = (index * _xLabelWidth + _chartMarginLeft + _xLabelWidth / 2.0);
NSInteger y = _chartMarginBottom + _chartCavanHeight;
*/
NSInteger x = (2*_x_axisMarginLeft + (index * _xLabelWidth)) - _xLabelWidth/2;
NSInteger y = _chartMarginTop + _chartCavanHeight;
PNChartLabel *label = [[PNChartLabel alloc] initWithFrame:CGRectMake(x, y, (NSInteger) _xLabelWidth, (NSInteger) _chartMarginBottom)];
[label setTextAlignment:NSTextAlignmentCenter];
label.text = labelText;
[self setCustomStyleForXLabel:label];
[self addSubview:label];
[_xChartLabels addObject:label];
}
}
个人理解如下:
_chartMarginLeft是控制图表绘制内容与图表整体视图的左间距,但源码中计算X轴坐标label的frame也使用了_chartMarginLeft,即如果通过_chartMarginLeft来调节,图表内容与X轴坐标label的位置都会发生改变。所以我在PNLineChart.h文件中增加了两项值,通过x_axisMarginLeft来代替_chartMarginLeft控制坐标label的位置:
// 用于在计算X轴坐标轴指示线和坐标值文本位置时区分chartMarginLeft和chartMarginRight
@property (nonatomic) CGFloat x_axisMarginLeft;
@property (nonatomic) CGFloat x_axisMarginRight;
在PNLineChart.m文件的 setupDefaultValues 初始化方法中,将其初始值设置为与_chartMarginLeft的初始值一致:
// add
_x_axisMarginLeft = 25.0;
_x_axisMarginRight = 25.0;
在PNLineChart.m文件的 drawRect: 方法中(大约在663行),刻度线的位置计算中也有_chartMarginLeft这一项,修改为通过_x_axisMarginLeft来控制刻度线的位置,保证刻度线始终在坐标值label的正上方:
// draw x axis separator
CGPoint point;
for (NSUInteger i = 0; i < [self.xLabels count]; i++) {
/* 源码
point = CGPointMake(2 * _chartMarginLeft + (i * _xLabelWidth), _chartMarginBottom + _chartCavanHeight);
*/
// 修改轴刻度线的位置
point = CGPointMake(2 * _x_axisMarginLeft + (i * _xLabelWidth), _chartMarginBottom + _chartCavanHeight);
CGContextMoveToPoint(ctx, point.x, point.y - 2);
CGContextAddLineToPoint(ctx, point.x, point.y);
CGContextStrokePath(ctx);
}
// draw y axis separator
CGFloat yStepHeight = _chartCavanHeight / _yLabelNum;
Y轴刻度线与刻度值位置不匹配也可以在这个方法中修改(// draw y axis separator 下面那一部分代码)
坐标轴单位名称
X轴的单位名称label原位置在轴线箭头右端,我修改源码(同样是 drawRect: 方法中)将X轴的单位名称label移到了轴线右端箭头的上方来方便显示,而且源码中限制的单位名称label长度较短,只能显示两个文字,此处也做了修改:
// 此处可修改轴单位名称字体大小
UIFont *font = [UIFont systemFontOfSize:11];
// draw y unit
if ([self.yUnit length]) {
/* 源码整体内容
CGFloat height = [PNLineChart sizeOfString:self.yUnit withWidth:30.f font:font].height;
CGRect drawRect = CGRectMake(_chartMarginLeft + 10 + 5, 0, 30.f, height);
[self drawTextInContext:ctx text:self.yUnit inRect:drawRect font:font];
*/
/**添加后整体内容**/
// 增加宽度最大值 防止单位较长显示不全
CGFloat height = [PNLineChart sizeOfString:self.yUnit withWidth:50.f font:font].height;
CGFloat width = [PNLineChart sizeOfString:self.yUnit withWidth:50.f font:font].width;
CGRect drawRect = CGRectMake(_x_axisMarginLeft + 10 + 5, 0, width, height);
[self drawTextInContext:ctx text:self.yUnit inRect:drawRect font:font];
/**添加后整体内容**/
}
// draw x unit
if ([self.xUnit length]) {
/* 源码
CGFloat height = [PNLineChart sizeOfString:self.xUnit withWidth:30.f font:font].height;
CGRect drawRect = CGRectMake(CGRectGetWidth(rect) - _chartMarginLeft + 5, _chartMarginBottom + _chartCavanHeight - height / 2, 25.f, height);
[self drawTextInContext:ctx text:self.xUnit inRect:drawRect font:font];
*/
/**添加后整体内容**/
CGFloat height = [PNLineChart sizeOfString:self.xUnit withWidth:50.f font:font].height;
CGFloat width = [PNLineChart sizeOfString:self.xUnit withWidth:50.f font:font].width;
// 将X轴单位位置移到箭头的上方 -5是为了防止label底部紧贴坐标轴
CGRect drawRect = CGRectMake(CGRectGetWidth(rect) - _chartMarginRight - width - 5, _chartMarginBottom + _chartCavanHeight - height - 5, width, height);
[self drawTextInContext:ctx text:self.xUnit inRect:drawRect font:font];
/**添加后整体内容**/
}
修改完成后,在适配图表内容显示时,根据需要改变_x_axisMarginLeft和chartMarginLeft这两项的值即可。
在之前的图表创建方法中修改如下:
- (void)loadBasicLineChart {
[self initializeLineChart];
// 图标可显示区域右边距
// X轴箭头过于靠近最后一个刻度值,可调节此项值
_lineChart.chartMarginRight = 0;
// 图表可显示区域左边距
// 图表内容与X轴内容对应有偏差,可调节此项或_x_axisMarginLeft
_lineChart.chartMarginLeft = 32;
// 是否显示 xy 坐标轴(默认不显示)
_lineChart.showCoordinateAxis = YES;
// 坐标轴宽度
_lineChart.axisWidth = 1;
结果如图:
result-02.png
多条曲线图
属于折线图的一种,增加一个LineChartData,如何添加图例详看代码:
- (void)loadSmoothLineChart {
[self initializeLineChart];
_lineChart.chartMarginRight = 5;
_lineChart.chartMarginLeft = 15;
_lineChart.chartCavanWidth = SCREEN_WIDTH-20;
_lineChart.chartCavanHeight = SCREEN_WIDTH-150;
_lineChart.x_axisMarginLeft = 17;
// Y轴相关设置
_lineChart.yLabelNum = 8;
_lineChart.yLabelColor = [UIColor clearColor];
_lineChart.yLabelFont = [UIFont systemFontOfSize:1];
_lineChart.showYGridLines = YES;
_lineChart.yGridLinesColor = SeparatorLineColor_Darker;
// X轴相关设置
_lineChart.xLabelColor = RGBColor(153, 153, 153);
_lineChart.xLabelFont = [UIFont systemFontOfSize:10];
[_lineChart setXLabels:@[@"一月", @"二月", @"三月", @"四月", @"五月", @"六月", @"七月", @"八月", @"九月", @"十月"]];
// 折线数据
NSArray *firstDataArray = @[@772, @956, @755, @791, @912, @1053, @1386, @1279, @1048, @885];
PNLineChartData *firstData = [PNLineChartData new];
firstData.color = PNGreen;
firstData.alpha = 0.7f;
firstData.lineWidth = 2.0;
firstData.itemCount = firstDataArray.count;
firstData.showPointLabel = YES;
firstData.inflexionPointWidth = 5.0f;
firstData.pointLabelColor = RGBColor(102, 102, 102);
firstData.pointLabelFont = [UIFont systemFontOfSize:10];
firstData.inflexionPointStyle = PNLineChartPointStyleSquare;
firstData.getData = ^(NSUInteger index) {
CGFloat yValue = [firstDataArray[index] floatValue];
return [PNLineChartDataItem dataItemWithY:yValue];
};
NSArray *secondDataArray = @[@1012, @1053, @922, @879, @953, @998, @1186, @1129, @1086, @951];
PNLineChartData *secondData = [PNLineChartData new];
secondData.color = PNBlue;
secondData.alpha = 0.7f;
secondData.lineWidth = 2.0;
secondData.itemCount = firstDataArray.count;
secondData.showPointLabel = YES;
secondData.inflexionPointWidth = 5.0f;
secondData.pointLabelColor = RGBColor(102, 102, 102);
secondData.pointLabelFont = [UIFont systemFontOfSize:10];
secondData.inflexionPointStyle = PNLineChartPointStyleTriangle;
secondData.getData = ^(NSUInteger index) {
CGFloat yValue = [secondDataArray[index] floatValue];
return [PNLineChartDataItem dataItemWithY:yValue];
};
_lineChart.chartData = @[firstData,secondData];
_lineChart.showSmoothLines = YES;
// 绘制图表
[_lineChart strokeChart];
// 设置代理,响应点击
_lineChart.delegate = self;
// 添加至视图
[self.view addSubview:_lineChart];
//添加图例
firstData.dataTitle =@"A产品销量 ";
secondData.dataTitle =@"B产品销量";
//横向显示
_lineChart.legendStyle = PNLegendItemStyleSerial;
// legendFontColor只能用系统规定颜色,其他方法获取颜色不可使用,会crash报错
_lineChart.legendFontColor = [UIColor darkGrayColor];
_lineChart.legendFont = [UIFont systemFontOfSize:12.0];
//图例所在位置
UIView *legend = [_lineChart getLegendWithMaxWidth:220];
[legend setFrame:CGRectMake(SCREEN_WIDTH/2-110, kNavHeight+55+(SCREEN_WIDTH-100)+5, legend.frame.size.width, legend.frame.size.height)];
//显示比例
_lineChart.hasLegend = YES;
//显示位置
_lineChart.legendPosition = PNLegendPositionBottom;
[self.view addSubview:legend];
}
可滑动的折线图
有的时候为了美观,每一项数据之间间隔较大,但数据项较多,需要将图标设计为可滑动样式,将图表创建在ScrollView上,通过数据项多少计算contentSize:
- (void)loadSwiperLineChart {
NSArray *x_axis = @[@"03-01", @"03-02", @"03-03", @"03-04", @"03-05", @"03-06", @"03-07", @"03-08", @"03-09", @"03-10", @"03-11", @"03-12", @"03-13", @"03-14", @"03-15"];
CGFloat maxWidth = MAX(15 + x_axis.count * 60.0 + 15, SCREEN_WIDTH-50);
self.chartBGScrollView.contentSize = CGSizeMake(maxWidth, self.chartBGScrollView.frame.size.height);
_lineChart = [[PNLineChart alloc] initWithFrame:CGRectMake(0, 0, self.chartBGScrollView.contentSize.width, self.chartBGScrollView.frame.size.height-35)];
_lineChart.backgroundColor = [UIColor clearColor];
_lineChart.chartMarginLeft = 15;
_lineChart.chartMarginRight = 15;
_lineChart.x_axisMarginLeft = 22;
// Y轴相关设置
_lineChart.yLabelColor = [UIColor clearColor];
_lineChart.yLabelFont = [UIFont systemFontOfSize:1];
// X轴相关设置
_lineChart.xLabelColor = RGBColor(153, 153, 153);
_lineChart.xLabelFont = [UIFont systemFontOfSize:10];
[_lineChart setXLabels:x_axis];
// 折线数据
NSArray *firstDataArray = @[@5, @8, @10, @6, @4, @1, @-3, @-1, @0, @1, @3, @5, @4, @0, @1];
PNLineChartData *firstData = [PNLineChartData new];
firstData.color = RGBColor(69, 140, 240);
firstData.lineWidth = 1.0;
firstData.itemCount = firstDataArray.count;
firstData.showPointLabel = YES;
firstData.inflexionPointWidth = 3.0f;
firstData.pointLabelColor = RGBColor(102, 102, 102);
firstData.pointLabelFont = [UIFont systemFontOfSize:10];
firstData.pointLabelFormat = @"%1.f°";
firstData.inflexionPointStyle = PNLineChartPointStyleCircle;
firstData.getData = ^(NSUInteger index) {
CGFloat yValue = [firstDataArray[index] floatValue];
return [PNLineChartDataItem dataItemWithY:yValue];
};
_lineChart.chartData = @[firstData];
_lineChart.showSmoothLines = YES;
// 绘制图表
[_lineChart strokeChart];
// 设置代理,响应点击
_lineChart.delegate = self;
// 添加至视图
[_chartBGScrollView addSubview:_lineChart];
// [self loadWeatherIconImgView];
}
柱形图
@property (nonatomic, strong) PNBarChart * barChart;
- (void)loadBarChart {
// 初始化
_barChart = [[PNBarChart alloc] initWithFrame:CGRectMake(0, kNavHeight+30, SCREEN_WIDTH, SCREEN_WIDTH-100)];
_barChart.backgroundColor = [UIColor clearColor];
// 是否显示xy轴的刻度值(默认不显示)
_barChart.showLabel = YES;
// 是否显示坐标轴(默认不显示)
_barChart.showChartBorder = YES;
// 柱子圆角值
_barChart.barRadius = 4;
// 柱子宽度值
_barChart.barWidth = 32;
// 柱子渲染颜色(柱子实体颜色)
_barChart.strokeColor = RGBColor(90, 144, 245);
// 柱子上是否显示数值(默认为YES)
_barChart.isShowNumbers = NO;
// 柱子是否立体显示(有渐变色效果,默认为YES)
_barChart.isGradientShow = NO;
_barChart.labelTextColor = RGBColor(102, 102, 102);
// 这部分设置尽量放在前面 放在后面可能导致坐标轴的刻度值不显示
// Y轴坐标值的宽度
_barChart.yChartLabelWidth = 20;
// Y轴距离图表最左侧的距离(左边距)
_barChart.chartMarginLeft = 25;
// X轴最右侧距离图表最右侧的距离(右边距)
_barChart.chartMarginRight = 5;
// 柱子最顶端距离图表最上方的距离(上边距)
_barChart.chartMarginTop = 5;
// X轴距离图表最底部的距离(下边距)
_barChart.chartMarginBottom = 10;
// X坐标刻度的上边距
_barChart.labelMarginTop = 5.0;
//设置bar Color
_barChart.xLabels = @[@"一",@"二",@"三",@"四",@"五",@"六"];
_barChart.yValues = @[@21, @16, @10, @24, @26, @14];
_barChart.yLabelFormatter = ^NSString*(CGFloat yLabelValue) {
return[NSString stringWithFormat:@"%f", yLabelValue];
};
//开始绘制
[_barChart strokeChart];
[self.view addSubview:_barChart];
}
饼图
@property (nonatomic, strong) PNPieChart * pieChart;
- (void)loadPieChart {
NSArray*items = @[
[PNPieChartDataItem dataItemWithValue:231 color:PNBlue description:@"支付宝 "],
[PNPieChartDataItem dataItemWithValue:198 color:PNGreen description:@"微信 "],
[PNPieChartDataItem dataItemWithValue:112 color:PNStarYellow description:@"信用卡 "],
[PNPieChartDataItem dataItemWithValue:137 color:PNLightBlue description:@"现金 "],
[PNPieChartDataItem dataItemWithValue:90 color:PNPinkDark description:@"其它"]];
PNPieChart *pieChart = [[PNPieChart alloc] initWithFrame:CGRectMake(70, kNavHeight+55, SCREEN_WIDTH-140, SCREEN_WIDTH-140) items:items];
// 饼图描述文字
pieChart.descriptionTextColor = [UIColor whiteColor];
// 设置文字字体
pieChart.descriptionTextFont = [UIFont fontWithName:@"Avenir-Medium" size:14];
// 文字阴影颜色 默认不显示阴影
// pieChart.descriptionTextShadowColor = [UIColor redColor];
// 显示实际数值,不显示实际比例
pieChart.showAbsoluteValues = NO;
// 只显示数值,不显示内容描述
pieChart.showOnlyValues = NO;
// showPullLine 显示指示线 新版本PNChart已取消
// 内外圆大小,此处设置无作用,需要修改源码或者新建类继承PNPieChart,类中重写recompute方法
//pieChart.innerCircleRadius = 0;
//pieChart.outerCircleRadius = 0;
[pieChart strokeChart];
//加到父视图上
[self.view addSubview:pieChart];
//显示比例
pieChart.hasLegend= YES;
//横向显示
pieChart.legendStyle= PNLegendItemStyleSerial;
pieChart.legendFontColor = [UIColor lightGrayColor]; // 不可以用RGB计算 只能使用系统指定颜色
pieChart.legendFont= [UIFont boldSystemFontOfSize:14];
//显示位置
pieChart.legendPosition= PNLegendPositionTop;
//获得图例,当横向排布不下另起一行
UIView*legend = [pieChart getLegendWithMaxWidth:SCREEN_WIDTH-70];
legend.frame= CGRectMake(35,kNavHeight+60+(SCREEN_WIDTH-140)+10, legend.bounds.size.width, legend.bounds.size.height);
[self.view addSubview:legend];
}
内外圆大小的设置。有些饼图要求展示效果为实心圆,这里我选择直接修改源码,也可以像注释中建议的继承类再重写方法:
在PNPieChart.m文件的 recompute 方法中(大约在102行)修改如下:
- (void)recompute {
/* 源码
self.outerCircleRadius = CGRectGetWidth(self.bounds) / 2;
self.innerCircleRadius = CGRectGetWidth(self.bounds) / 6;
*/
// 设备饼图的内部圆大小与外部圆大小 可以通通过自定义一个类继承PieChart,在这个类中重写recompute方法
self.outerCircleRadius = CGRectGetWidth(self.bounds) / 2;
// 设置内部空心圆为0
self.innerCircleRadius = 0;
}
效果如图:
result-03.png
也可以在这个方法里修改内圆的半径大小,达到需要的展示效果。
代理方法
#pragma mark - Delegate
- (void)userClickedOnLinePoint:(CGPoint)point lineIndex:(NSInteger)lineIndex {
NSLog(@"chosed lineIndex(线 下标) %ld", lineIndex);
}
- (void)userClickedOnLineKeyPoint:(CGPoint)point lineIndex:(NSInteger)lineIndex pointIndex:(NSInteger)pointIndex {
NSLog(@"lineIndex(第几根线):%ld 选择点的pointIndex %ld",lineIndex + 1,pointIndex);
//NSLog(@"X:%@ Y:%@",_chart_X_LabelArr[pointIndex],_chart_Y_LabelArr[pointIndex]);
}