需求简介
类似支付宝那样的钱包工具,有很大一个类别理财,经常出现收益率曲线之类的,这个就是折线图,大概的样子如下
如何实现
采用贝塞尔曲线自己画,采用
CAShapeLayer
进行组合,也是可以做的。只是有点复杂,操心的内容有点多。采用第三方库,这也是一个好主意。有现实需求,自己实现又烦,好用的第三方库就来了。经过比较,我们可以选择 Charts
这个第三方库是用
swift
写的,在目前的object-c
工程中使用有点麻烦。不过,为了和Android
保持一致,这点麻烦算不了什么。这些纠结的过程,已经在另一篇文章里写了:关于图表库的选择
包装Charts
Charts
中代表折线图的是LineChartView
,从BarLineChartViewBase,ChartViewBase,NSUIView,UIView
一路继承过来。
- 创建一个内部属性成员
LineChartView
,作为我们的自定义view
的子view
,并且占满所有空间。我们的自定义view只是一个容器。
@interface KJTLineChartView ()
@property (strong, nonatomic) LineChartView *lineView;
@end
- 创建子视图
#pragma mark - private
- (void)setup {
self.lineView = [[LineChartView alloc] init];
[self addSubview:self.lineView];
}
- 自定义视图只是作为容器,将刚刚创建的折线图铺满整个空间。
// 布局
- (void)layoutSubviews {
[self.lineView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(0);
make.bottom.mas_equalTo(0);
make.left.mas_equalTo(0);
make.right.mas_equalTo(0);
}];
}
- 由于没有数据,所以现在是空白:
- 图中的文案,在
ChartViewBase
中可以找到:
设置数据
图中数据点的包含
(x,y)
两个维度的信息,数据类型都是double
。在大多数的介绍文章中,都是把x
轴的信息简化为序号index
,这当然是可以的。这次的需求是一张“七日年化收益率”折线图,'y'轴是收益率,数据类型是
double
,没问题。横轴是七天的日期,是“月月-日日”的格式。考虑到NSDate
类型转化为double
和字符串都很方便,所以x
轴数据可以直接是“时间戳”,也就是从1970
年到现在所过的秒数。所以这次的做法是将NSDate
类型转化为double
,直接作为x
轴数据来实现的。关于数据,设置过程主要有以下几步
- 需要封装成
一个ChartDataEntry
结构; - 多个
ChartDataEntry
然后形成一个数组之后; - 将数组再包装成一个
LineChartDataSet
结构,这个结构代表一条曲线; - 将多个(包括1个)
LineChartDataSet
结构包装成一个数组,所以可以同时显示多条曲线; - 将这个数组包装成
LineChartData
结构; - 用得到的这个结构,赋值给
LineChartView的data
成员;
- 设计了一个对外接口,提供标签数组和y值数组
// x轴是日期,y轴是数据
- (void)updateWithDates:(NSArray *)dates values:(NSArray *)values {
//
// 日期和数据组成一个点,形成一条折线
//
// 1. 将日期和数据取出,包装成ChartDataEntry,组成新的数组
// x轴是日期, y轴是数据
NSMutableArray *entrys = [NSMutableArray array];
NSInteger count = dates.count;
for (NSInteger index = 0; index < count; index++) {
double x = (double)dates[index].timeIntervalSince1970;
double y = [values[index] doubleValue];
ChartDataEntry *entry = [[ChartDataEntry alloc] initWithX:x y:y];
[entrys addObject:entry];
}
// 2. 组装LineChartDataSet结构,并做相应的配置
LineChartDataSet *dataSet = [[LineChartDataSet alloc] initWithValues:entrys label:nil];
// 配置先不做,看默认效果
// 3. 组装LineChartDataSet数组,这里只需要一个dataSet
NSArray *dataSets = [NSArray arrayWithObject:dataSet];
// 4. 组装LineChartData结构
LineChartData *data = [[LineChartData alloc] initWithDataSets:dataSets];
// 5. 赋值
self.lineView.data = data;
}
- 在调用的ViewController中,给几个测试数据
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
NSDate *today = [NSDate date];
NSArray *dates = @[today, [today kjtDateByAddingDays:1], [today kjtDateByAddingDays:2], [today kjtDateByAddingDays:3], [today kjtDateByAddingDays:4], [today kjtDateByAddingDays:5], [today kjtDateByAddingDays:6]];
NSArray *values = @[@3.729, @3.683, @2.8, @3.1, @4.123, @3.587, @3.7482];
[self.lineChartView updateWithDates:dates values:values];
}
运行起来之后,效果是这样的:
整体设置
默认的可以拖动,可以放大,双击也可以放大,y
轴有两个。在这里,这些都不需要,可以做如下配置:
//
// 设置chart view 整体
//
// 选中处理等
self.lineView.delegate = self;
// 隐藏说明图标
self.lineView.legend.form = ChartLegendFormNone;
// 无数据提示信息
self.lineView.noDataText = @"暂无数据";
// 取消Y轴缩放
self.lineView.scaleYEnabled = NO;
// 取消X轴缩放
self.lineView.scaleXEnabled = NO;
// 取消双击缩放
self.lineView.doubleTapToZoomEnabled = NO;
// 取消拖拽图标
self.lineView.dragEnabled = NO;
// 加载动画
[self.lineView animateWithXAxisDuration:1.0];
// 隐藏右Y轴
self.lineView.rightAxis.enabled = NO;
加上这些配置之后,右y
轴没了,也不能放大了,legend
也没有了,并且还有生成动画。大致效果如下:
配置左Y轴
根据设计UI
图,配置左Y
轴的相关属性
//
// 设置左Y轴
//
// y轴展示多少个
self.lineView.leftAxis.labelCount = 6;
// 设置Y轴的最小值
self.lineView.leftAxis.axisMinimum = 0;
// Y轴颜色
self.lineView.leftAxis.axisLineColor = kColorWithHexA(0x000000, 0.24);
// 文字颜色
self.lineView.leftAxis.labelTextColor = kColorWithHexA(0x000000, 0.24);
// 文字字体
self.lineView.leftAxis.labelFont = kFontSystem10;
// 网格线颜色
self.lineView.leftAxis.gridColor = kColorWithHex(0xECECF1);
这时的效果如下:
配置X轴
//
// 设置X轴
//
// 设置x轴数据在底部
self.lineView.xAxis.labelPosition = XAxisLabelPositionBottom;
// x轴不画网格线
self.lineView.xAxis.drawGridLinesEnabled = NO;
// X轴颜色
self.lineView.xAxis.axisLineColor = kColorWithHexA(0x000000, 0.24);
// 文字颜色
self.lineView.xAxis.labelTextColor = kColorWithHexA(0x000000, 0.24);
// 文字字体
self.lineView.xAxis.labelFont = kFontSystem10;
配置折线
// 是否在拐点处显示数据
dataSet.drawValuesEnabled = NO;
// 折线宽度
dataSet.lineWidth = 2;
// 折线颜色
[dataSet setColor:kRedColor];
// 是否绘制拐点
dataSet.drawCirclesEnabled = NO;
// 是否填充颜色
dataSet.drawFilledEnabled = YES;
// 填充颜色
dataSet.fillColor = kRedColor;
// 填充颜色透明度
dataSet.fillAlpha = 0.20;
// 去掉左下角“DataSet”字符串
dataSet.label = nil;
格式化坐标轴标签
标签格式的设置比较特殊,需要一个单独的类,实现IChartAxisValueFormatter
协议。只有一个函数
-
Y
轴格式化,将double
类型的数据,转化为3
位小数的字符串。并且为了避免拥挤,0
不显示
@implementation KJTLabelFormatterY
#pragma mark - IChartAxisValueFormatter
- (NSString *)stringForValue:(double)value axis:(ChartAxisBase *)axis {
// 第一个Y轴坐标不显示,避免和第一个X坐标拥挤
if (value == 0) {
return @"";
}
return [NSString stringWithFormat:@"%0.3f", value];
}
@end
配置的地方,指定这个自定义的类
// 格式化y标签
self.lineView.leftAxis.valueFormatter = [[KJTLabelFormatterY alloc] init];
- X轴格式化,这里就很简单,将时间戳转换为“月月-日日”格式的日期字符串就可以了。
@implementation KJTLabelFormatterX
#pragma mark - IChartAxisValueFormatter
- (NSString *)stringForValue:(double)value axis:(ChartAxisBase *)axis {
NSDate *date = [NSDate dateWithTimeIntervalSince1970:(NSTimeInterval)value];
return [date kjtStringWithFormat:@"MM-dd"];
}
@end
配置的地方:
// 格式化x轴标签
self.lineView.xAxis.valueFormatter = [[KJTLabelFormatterX alloc] init];
设置选中标签
提供了一个ChartMarkerView
视图,作为选中之后弹出的“气泡视图”。这个可以看做一个容器,具体的样子,完全可以自定义。这里,就用了一个UILabel
和一个UIImageView
来实现。
很多文章中提到的BalloonMarker
并不存在,这是官方demo
中自己写的一个自定义视图,并且是swift
的,不能直接用。
“气泡视图”ChartMarkerView
的原点,默认在选中数据点上,效果会出现在下方。如果要做到UI
上那样在选中点上方,需要通过offset
属性来调节位置。这个地方相对布局不方便,直接用frame
比较好。这个过程相对比较繁琐,需要耐心调整。
//
// 设置选中时候的"气泡视图"
//
// 这个标记视图将以数据点为起点布局
ChartMarkerView *markerView = [[ChartMarkerView alloc] init];
// 显示y轴数据的标签
self.markerLabel = [[UILabel alloc] init];
self.markerLabel.layer.cornerRadius = 9;
self.markerLabel.layer.masksToBounds = YES;
self.markerLabel.font = kFontSystem12;
self.markerLabel.textAlignment = NSTextAlignmentCenter;
self.markerLabel.textColor = [UIColor whiteColor];
self.markerLabel.backgroundColor = kRedColor;
[markerView addSubview:self.markerLabel];
// 选中小图标
UIImageView *imageView = [[UIImageView alloc] init];
imageView.image = [UIImage imageNamed:@"折线图选中"];
[markerView addSubview:imageView];
// 布局,标签和图标右对齐,间隔为4
self.markerLabel.frame = CGRectMake(0, 0, 49, 18);
imageView.frame = CGRectMake(49-14, 18+4, 14, 14);
// 设置偏移量,使数据点和小图标的中心点重合
CGFloat x = imageView.center.x;
CGFloat y = imageView.center.y;
markerView.offset = CGPointMake(-x, -(y+7));
// 设置折线图的标签视图
markerView.chartView = self.lineView;
self.lineView.marker = markerView;
UILabel
上要显示y的值,需要实现ChartViewDelegate
协议
#pragma mark - ChartViewDelegate
- (void)chartValueSelected:(ChartViewBase *)chartView entry:(ChartDataEntry *)entry highlight:(ChartHighlight *)highlight {
self.markerLabel.text = [NSString stringWithFormat:@"%0.4f", entry.y];
}
效果图如下,和UI
设计图比较接近了。
- 在顶部,如果空间不够,由于受到边界影响,气泡视图会出现移位现象,导致图片对不准。
- 第
1
个点,由于有坐标轴的影响,位置也会偏移一些
-
UI
图上是"山顶",效果相对较好,如果位置出现在"山谷",效果就差一点了
小结
相对于自己来写,省力很多,整体效果也好很多。虽然配置比较麻烦,思路有点奇特。推荐使用。
参考文章
iOS Charts框架集成及使用
iOS-Charts看这个就够了
iOS使用Charts框架绘制折线图
ios-charts之折线图