JBChartView源码解读

JBChartView是一款iOS图表库,包含折线图和直方图。使用方便且可定制高。JBChartView所有的实现都类似UITableView的数据源和委托模式,所以说如果你对UITableView或者UITabelViewController的使用比较熟悉,使用JBChartView将会轻而易举。

JBChartView包含折线图和直方图,本文主要记录对barChart直方图的源码学习。JBBarChartView是直方图的具体类型,要创建该对象只需简单几步:

  1. 初始化并设置数据源和代理
1
JBBarChartView *barChartView = [[JBBarChartView alloc] init];
barChartView.dataSource = self;
barChartView.delegate = self;
[self addSubview:barChartView];
  1. 析构时清除相关属性
1
- (void)dealloc
{
	JBBarChartView *barChartView = ...; // i.e. _barChartView
	barChartView.delegate = nil;
	barChartView.dataSource = nil;
}
  1. 实现必须的数据源和委托方法
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把直方图的绘制分成了多个步骤,分别为:

  1. 创建数据模型
  2. 获取bar间距
  3. 创建选择View(垂直)
  4. 创建和固定bar view(s)
  5. 布局头和尾视图
  6. 刷新图表状态

下面详细讲解每个步骤是如何实现的。

创建数据模型

首先通过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];
}