JBChartView是一款iOS图表库,包含折线图和直方图。使用方便且可定制高。JBChartView所有的实现都类似UITableView的数据源和委托模式,所以说如果你对UITableView或者UITabelViewController的使用比较熟悉,使用JBChartView将会轻而易举。
JBChartView包含折线图和直方图,本文主要记录对barChart直方图的源码学习。JBBarChartView是直方图的具体类型,要创建该对象只需简单几步:
- 初始化并设置数据源和代理
1 | JBBarChartView *barChartView = [[JBBarChartView alloc] init]; barChartView.dataSource = self; barChartView.delegate = self; [self addSubview:barChartView]; |
- 析构时清除相关属性
1 | - (void)dealloc { JBBarChartView *barChartView = ...; // i.e. _barChartView barChartView.delegate = nil; barChartView.dataSource = nil; } |
- 实现必须的数据源和委托方法
1 | //datasource - (NSUInteger)numberOfBarsInBarChartView:(JBBarChartView *)barChartView { return ...; // number of bars in chart } //delegate - (CGFloat)barChartView:(JBBarChartView *)barChartView heightForBarViewAtIndex:(NSUInteger)index { return ...; // height of bar at index } |
4.确保设置了barChartView的frame及至少调用了一次reloadData方法
1 | barChartView.frame = CGRectMake( ... ); [barChartView reloadData]; |
##直方图绘制
上述只是介绍了如何使用JBBarChartView,那JBBarChartView是如何实现直方图的绘制呢?一切还得从[barChartView reloadData]说起。reloadData把直方图的绘制分成了多个步骤,分别为:
- 创建数据模型
- 获取bar间距
- 创建选择View(垂直)
- 创建和固定bar view(s)
- 布局头和尾视图
- 刷新图表状态
下面详细讲解每个步骤是如何实现的。
创建数据模型
首先通过datasource获取bar的个数,然后通过delgate获取每一个bar的高度,把这些bar的height存放进NSArray即完成了数据模型的创建。大体代码逻辑如下:
1 | NSUInteger dataCount = [self.dataSource numberOfBarsInBarChartView:self]; NSMutableArray *mutableChartData = [NSMutableArray array]; for (NSUInteger index=0; index<dataCount; index++) { CGFloat height = [self.delegate barChartView:self heightForBarViewAtIndex:index]; [mutableChartData addObject:[NSNumber numberWithFloat:height]]; } self.chartData = [NSArray arrayWithArray:mutableChartData]; |
####获取bar间距
为了绘制直方图,需要获取bar间距以便设置每一个bar的x坐标。
首先查看delegate是否指定了barPadding,如果指定了就用delegate的,否则就用一个自定义的小算法计算barPadding(50*总bar数的倒数),相关代码如下:
1 | NSUInteger totalBars = [self.chartData count]; self.barPadding = (1/(float)totalBars) * kJBBarChartViewBarBasePaddingMutliplier; |
创建选择View(垂直)
首先计算选择View的Height(self.bounds.size.height-self.headerView.frame.size.height - self.footerView.frame.size.height - self.headerPadding - self.footerPadding),如果datasource设置了允许延伸到Header或者Footer,那么verticalSelectionViewHeight需要再加上相关的padding。然后计算选择View的宽度(整个CharView的宽度-所有barPadding的宽度,然后再除以bar的个数),得到高度和宽度后创建选择View(JBChartVerticalSelectionView),最后添加到CharView上。
创建和固定bar view(s)
遍历数据模型,每一个模型点创建一个bar,具体创建bar的过程如下:
首先查看datasource、delegate是否创建了barView,如果创建了就使用(具体优先级为:delegate的barGradient > delegate的colorForBar > dataSource的barView),如果没创建就新建一个UIView,然后设置bar view的frame,bar的高度如何计算呢?JBBarChartView使用了归一化高度,即根据数据模型里面最大值、最小值及JBChartView可用绘图的高度计算出每一个bar最终显示的在View上的高度,归一化的相关代码如下:
1 | - (CGFloat)normalizedHeightForRawHeight:(NSNumber *)rawHeight { CGFloat minHeight = [self minimumValue]; CGFloat maxHeight = [self maximumValue]; CGFloat value = [rawHeight floatValue]; if ((maxHeight - minHeight) <= 0) { return [self availableHeight]; } return ((value - minHeight) / (maxHeight - minHeight)) * [self availableHeight]; } |
bar view的x坐标就根据bar的宽度和barPadding计算得出,最后设置barView的frame,然后把barView添加到JBBarChartView上。
布局头和尾视图
因header和footer要添加到JBBarChartView上,故而需要对它们的frame做一些调整(相对布局),具体代码如下:
1 | dispatch_block_t layoutHeaderAndFooterBlock = ^{ self.headerView.frame = CGRectMake(self.bounds.origin.x, self.bounds.origin.y, self.bounds.size.width, self.headerView.frame.size.height); self.footerView.frame = CGRectMake(self.bounds.origin.x, self.bounds.size.height - self.footerView.frame.size.height, self.bounds.size.width, self.footerView.frame.size.height); }; |
刷新图表状态
主要刷新了当前状态(JBChartViewStateExpanded或者JBChartViewStateCollapsed),及关闭了reloading标志位,表明可以执行reloaddata。
触摸事件
当用户触摸图表时,JBBarChartView会把当状态(当前触摸了哪个bar、哪个bar取消触摸了)发送给delegate。具体实现是利用touches方法,来检测触摸开始、移动、结束或者取消状态,触摸开始、移动时根据触摸点Point来判断当前触摸的是哪个bar,实时发送给delegate,当结束或者取消时也根据最后的触摸点Point来计算出是哪个bar实时发送给delegate。具体代码可参考如下4个方法:
1 | #pragma mark - Touches - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { [self setVerticalSelectionViewVisible:NO animated:NO]; [self touchesBeganOrMovedWithTouches:touches]; } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { [self touchesBeganOrMovedWithTouches:touches]; } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { [self touchesEndedOrCancelledWithTouches:touches]; } - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { [self touchesEndedOrCancelledWithTouches:touches]; } |