CoreGraphics绘制图表教程(三)

本篇介绍如何使用CoreGraphics绘制柱状图、圆饼图、环形图以及区域填充

柱状图



圆饼图



环形图



区域填充



有了前两篇的基础,绘制后面这些图表就简单的非常了。在第一篇的绘图三板斧中介绍了所有绘制图表所需的必要技巧:

  • 绘制折线图:就是三板斧中的画一条线,图表中多画了几条线段而已

  • 绘制柱状图:就是三板斧中绘制一个矩形和填充一个矩形,图表中多画了几个矩形,多填充了几个矩形区域

  • 绘制圆饼图:就是三板斧中的画一个又一个扇形,然后填充,最后找到准确的位置写上标题

  • 绘制环形图:这个就更简单了,就是画几段圆弧,把圆弧的宽度加大,就成了圆环

  • 绘制区域填充:把画的线闭合,然后填充,就行了

这几种图表过于简单,所以在这最后一篇集中来写了,一口气就都说完吧。

柱状图

柱状图需要显示坐标系,所以要继承自 CoordinateSystem 。然后要给柱状图的类起个名字,就叫 BarChart,别问我为什么叫这个,有道词典说得这么起名。柱状图的数据类也要起个名字,就叫 BarChartData 吧,这样有辨识度。

先统一一下思想, BarChart 代表的是这张柱状图, BarChartData 保存的是每一根 “柱体” 的具体信息

ok,两个类建立好了之后,要思考一下都有什么属性或者方法。先看 BarChart 类。

#import "CoordinateSystem.h"

@class BarChartData;
@interface BarChart : CoordinateSystem

@property (nonatomic, strong) NSMutableArray<BarChartData *> *dataArray;
@property (nonatomic, assign) CGFloat barSpacing;
@property (nonatomic, assign) CGFloat barLeftInset;
@property (nonatomic, assign) CGFloat barRightInset;

@end

柱状图 BarChart 需要有一个数据集合 NSMutableArray<BarChartData *> *dataArray ,用来保存将要绘制的 n 个 “柱体” 的具体信息。然后就是一些常规的属性:

  • CGFloat barSpacing 柱体间距

  • CGFloat barLeftInset 最左侧柱体与Y轴(坐标系左边界)的间距

  • CGFloat barRightInset 最右侧柱体到坐标系右边界的距离


看完 BarChart 类,接下来看 BarChartData 类需要来点什么属性

#import <UIKit/UIKit.h>

@interface BarChartData : NSObject

@property (nonatomic, copy) NSString *valueForX;
@property (nonatomic, copy) NSString *valueForY;
@property (nonatomic, strong) UIColor *borderColor;
@property (nonatomic, strong) UIColor *fillColor;
@property (nonatomic, assign) CGFloat borderWidth;

- (instancetype)initWithValueForX:(NSString *)valueForX valueForY:(NSString *)valueForY;

@end
  • NSString *valueForX X轴刻度

  • NSString *valueForY Y轴刻度

  • UIColor *borderColor 柱体边框线颜色

  • CGFloat borderWidth 柱体宽度

  • UIColor *fillColor 柱体填充颜色

外加一个初始化方法,搞定!



准备工作做得差不多了,下面开始进入正题。

与折线图一样,需要先确定坐标系。Y轴不变,正常绘制纬线和刻度,先计算取值范围,就是上一篇中的三步走,把最大值和最小值一通修改得到的取值范围,用这个取值范围来确定Y轴的刻度。

X轴需要重写一下绘制刻度的方法,用子类方法覆盖父类方法的方式,就不用再自己调用一遍了。柱状图的效果图大家也看到了,之所以重写是因为柱状图需要分别绘制上下标题(刻度)。柱体上方的标题是它的Y轴刻度,下方的是它的X轴刻度。


BarChart 类的 drawRect: 方法中:

- (void)drawRect:(CGRect)rect {
    // 计算Y轴取值范围
    [self calcValueRangeForY];
    [self initAxisY];
    [self initAxisX];
    // 初始化父类 drawRect
    [super drawRect:rect];
    
}

initAxisY 方法和 initAxisX 方法与折线图中的思路完全一致,目的还是要确定取值范围,然后确定Y轴刻度和X轴刻度从而绘制坐标系。不过这里的坐标系不需要经线,CoordinateSystem 中经线默认是不显示的,所以只要不给赋值YES就行了。

/**
 显示经线
 */
@property (nonatomic, assign) BOOL displayLongitude;

之后就是父类的 drawRect: 方法开始执行,绘制坐标系。坐标系画完后会调用预留接口 drawData: 方法,在重写的 drawData: 方法中可以绘制柱体:

- (void)drawData:(CGRect)rect {
    // 绘制数据
    [self drawBarChart:rect];
}

#pragma mark - 绘制柱体
- (void)drawBarChart:(CGRect)rect {
    CGFloat barWidth = (rect.size.width - self.axisMarginLeft - self.axisMarginRight - self.barLeftInset - self.barRightInset - self.barSpacing * (self.dataArray.count - 1)) / self.dataArray.count;
    
    CGContextRef context = UIGraphicsGetCurrentContext();
    
    if (self.dataArray && self.dataArray.count) {
        CGFloat valueX = self.axisMarginLeft + self.barLeftInset;
        for (BarChartData *barData in self.dataArray) {
            if (barData) {
                CGFloat valueY = (1 - (barData.valueForY.floatValue - self.minValue)/(self.maxValue - self.minValue)) * (rect.size.height - self.axisMarginBottom - self.axisMarginTop) + self.axisMarginTop;
                
                CGRect drawFrame = CGRectMake(valueX, valueY, barWidth, rect.size.height - valueY - self.axisMarginBottom);
                CGContextSetLineWidth(context, barData.borderWidth);
                CGContextSetStrokeColorWithColor(context, barData.borderColor.CGColor);
                CGContextSetFillColorWithColor(context, barData.fillColor.CGColor);
                CGContextFillRect(context, drawFrame);
                CGContextStrokeRect(context, drawFrame);
            }
            //X位移
            valueX = valueX + self.barSpacing + barWidth;
        }
    }
}

BarChart 类中需要重写父类的绘制标题方法:

#pragma mark - 绘制柱条上下标题
- (void)drawXAxisTitles:(CGRect)rect {
    
    if ([self.longitudeTitles count] <= 0) {
        return;
    }
    
    CGFloat barWidth = (rect.size.width - self.axisMarginLeft - self.axisMarginRight - self.barLeftInset - self.barRightInset - self.barSpacing * (self.dataArray.count - 1)) / self.dataArray.count;
    
    CGFloat barCenterX = self.axisMarginLeft + self.barLeftInset + barWidth/2;
    
    for (int i = 0; i < [self.longitudeTitles count]; i++) {
        // 取出上下标题
        BarChartData *barData = self.dataArray[i];
        NSString *topTitle = [self formatYTitles:barData.valueForY.longLongValue];
        NSString *bottomTitle = (NSString *) [self.longitudeTitles objectAtIndex:i];
        // 统一设置属性
        UIFont *textFont= self.longitudeFont; //设置字体
        NSMutableParagraphStyle *textStyle=[[NSMutableParagraphStyle alloc]init];//段落样式
        textStyle.lineBreakMode = NSLineBreakByWordWrapping;
        
        NSDictionary *attrs = @{NSFontAttributeName:textFont,
                                NSParagraphStyleAttributeName:textStyle,
                                NSForegroundColorAttributeName:self.longitudeFontColor};
        // 绘制下标题
        CGSize bottomTitleSize = [bottomTitle boundingRectWithSize:CGSizeMake(100, 100)
                                                 options:NSStringDrawingUsesLineFragmentOrigin
                                                 attributes:attrs
                                                 context:nil].size;
        CGRect bottomTitleRect= CGRectMake(barCenterX - bottomTitleSize.width/2, rect.size.height - self.axisMarginBottom, bottomTitleSize.width, bottomTitleSize.height);
        textStyle.alignment=NSTextAlignmentCenter;
        [bottomTitle drawInRect:bottomTitleRect withAttributes:attrs];
        
        // 绘制上标题
        CGSize topTitleSize = [topTitle boundingRectWithSize:CGSizeMake(100, 100)
                                                    options:NSStringDrawingUsesLineFragmentOrigin
                                                    attributes:attrs
                                                    context:nil].size;
        CGFloat valueY = (1 - (barData.valueForY.floatValue - self.minValue)/(self.maxValue - self.minValue)) * (rect.size.height - self.axisMarginBottom - self.axisMarginTop) + self.axisMarginTop - topTitleSize.height;
        
        CGRect topTitleRect= CGRectMake(barCenterX - topTitleSize.width/2, valueY, topTitleSize.width, topTitleSize.height);
        textStyle.alignment=NSTextAlignmentCenter;
        [topTitle drawInRect:topTitleRect withAttributes:attrs];
        
        // X位移
        barCenterX = barCenterX + self.barSpacing + barWidth;
    }
}

- (NSString *)formatYTitles:(long)value {
    if (value >= 10000) {
        return [NSString stringWithFormat:@"%.2f 万", value/10000.0f];
    }
    else {
        return [NSString stringWithFormat:@"%ld", value];
    }
}

最后为表示对我大秦帝国历代君王的追思,我特意把十字交叉线的标题做了完善,之前折线图中的十字交叉线的标题都是显示的百分比,没有具体完善。代码如下:

先在 CoordinateSystem 类中搞一个协议,为的是让子类可以自定义十字交叉线的标题,子类可以返回一个只有两个元素的数组,数组的第一个元素是十字交叉线X轴标题,数组的最后一个元素是十字交叉线Y轴标题。


@protocol CoordinateSystemDelegate <NSObject>

- (NSArray *)crossLineTouchPoint:(CGPoint)touchPoint xPercent:(CGFloat)xPercent yPercent:(CGFloat)yPercent frame:(CGRect)rect;

@end

然后修改一下 CoordinateSystem 类中获取十字交叉线XY轴刻度的方法,只要子类实现代理方法并返回标题,十字交叉线的XY轴刻度就会按照返回值显示:

// 获取十字交叉线的X轴刻度
- (NSString *)calcAxisXGraduate:(CGRect)rect {
    if ([self.delegate respondsToSelector:@selector(crossLineTouchPoint:xPercent:yPercent:frame:)]) {
        NSArray *titles = [self.delegate crossLineTouchPoint:self.singleTouchPoint xPercent:[self touchPointAxisXValue:rect] yPercent:[self touchPointAxisYValue:rect] frame:rect];
        return titles.firstObject;
    }
    return [NSString stringWithFormat:@"%f", [self touchPointAxisXValue:rect]];
}

// 获取十字交叉线的Y轴刻度
- (NSString *)calcAxisYGraduate:(CGRect)rect {
    if ([self.delegate respondsToSelector:@selector(crossLineTouchPoint:xPercent:yPercent:frame:)]) {
        NSArray *titles = [self.delegate crossLineTouchPoint:self.singleTouchPoint xPercent:[self touchPointAxisXValue:rect] yPercent:[self touchPointAxisYValue:rect] frame:rect];
        return titles.lastObject;
    }
    return [NSString stringWithFormat:@"%f", [self touchPointAxisYValue:rect]];
}



然后回到柱状图这里,实现代理,确定十字交叉线的XY轴标题(刻度)。这里进行了柱间区域的判断,比如 “嬴稷” 和 “嬴政” 之间的留空区是不显示X轴标题的,因为那部分本来就没有X轴对应的标题,就是一块空白。

#pragma mark - 十字交叉线重绘标题
- (NSArray *)crossLineTouchPoint:(CGPoint)touchPoint xPercent:(CGFloat)xPercent yPercent:(CGFloat)yPercent frame:(CGRect)rect {
    NSMutableArray *arrM = [NSMutableArray array];
    // 处理X轴
    CGFloat barWidth = (rect.size.width - self.axisMarginLeft - self.axisMarginRight - self.barLeftInset - self.barRightInset - self.barSpacing * (self.dataArray.count - 1)) / self.dataArray.count;
    CGFloat barLeft = self.axisMarginLeft + self.barLeftInset;
    for (int i = 0; i < self.dataArray.count; i++) {
        CGFloat barRight = barLeft + barWidth;
        if (touchPoint.x >= barLeft && touchPoint.x < barRight) {
            BarChartData *data = self.dataArray[i];
            [arrM addObject:data.valueForX];
            break;
        }
        barLeft = barLeft + barWidth + self.barSpacing;
    }
    
    if (arrM.count == 0) {
        [arrM addObject:@""];
    }
    
    // 处理Y轴
    CGFloat valueRange = self.maxValue - self.minValue;
    CGFloat valueY = valueRange * yPercent;
    NSString *yString = [self formatYTitles:valueY];
    [arrM addObject:yString];
    
    return arrM;
}
柱状图最终效果

OK! 柱状图大功告成



圆饼图

绘制圆饼图不需要显示坐标系,所以圆饼图只需继承 UIView 。给它也取个名字,就叫 PieChart 。圆饼图的数据类也取个名字,叫 PieChartData,一个非常优雅的名字。

看看圆饼图都需要哪些属性:

#import <UIKit/UIKit.h>

@class PieChartData;
@interface PieChart : UIView

@property (nonatomic, strong) NSMutableArray<PieChartData *> *dataArray;

/**
 边框线宽
 */
@property (nonatomic, assign) CGFloat borderWidth;

/**
 边框线颜色
 */
@property (nonatomic, strong) UIColor *borderColor;

/**
 标题颜色
 */
@property (nonatomic, strong) UIColor *titleColor;

/**
 标题字号
 */
@property (nonatomic, strong) UIFont *titleFont;

@end

再看看圆饼图数据类需要哪些属性:

#import <UIKit/UIKit.h>

@interface PieChartData : NSObject

@property (nonatomic, copy) NSString *title;
@property (nonatomic, assign) CGFloat value;
@property (nonatomic, strong) UIColor *fillColor;

- (instancetype)initWithValue:(CGFloat)value fillColor:(UIColor *)fillColor title:(NSString *)title;

@end
  • NSString *title 每个扇形区域的标题

  • CGFloat value 每个扇形区域的数值

  • UIColor *fillColor 每个扇形区域的填充色,这个属性放在 PieChart 中也可以,看具体需求而定


回到 PieChart 类中,先初始化各个属性,给定默认值:

// 给定圆周率 π 的值
#define PI 3.141592653f

- (id)init {
    self = [super init];
    if (self) {
        //初始化属性
        [self initProperty];
    }
    return self;
}

- (id)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    if (self) {
        //初始化属性
        [self initProperty];
    }
    return self;
}

- (id)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        self.frame = frame;
        //初始化属性
        [self initProperty];
    }
    return self;
}

- (void)initProperty {
    self.borderColor = [UIColor whiteColor];
    self.borderWidth = 1;
    self.titleFont = [UIFont systemFontOfSize:14];
    self.titleColor = [UIColor blackColor];
}



画圆饼图

- (void)drawRect:(CGRect)rect {
    if (!self.dataArray || !self.dataArray.count) {
        return;
    }
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetLineWidth(context, self.borderWidth);
    CGContextSetAllowsAntialiasing(context, YES);
    
    // 数据总和
    CGFloat total = [self calcTotalValue];
    // 起始位置弧度
    CGFloat offset = PI * -0.5;
    // 半径和圆心
    CGFloat radius = MIN(rect.size.width, rect.size.height) / 2;
    CGPoint circleCenter = CGPointMake(rect.size.width/2, rect.size.height/2);
    
    for (PieChartData *data in self.dataArray) {
        if (data) {
            /*
             填充扇形区域
             */

            // 拿出数据
            CGFloat value = data.value;
            // 计算百分比
            CGFloat percent = value / total;
            // 确定分支弧度
            CGFloat endAngle = percent * 2 * PI;
            // 设定起始点为圆心
            CGContextMoveToPoint(context, circleCenter.x, circleCenter.y);
            // 添加一个圆
            CGContextAddArc(context, circleCenter.x, circleCenter.y, radius, offset, endAngle + offset, 0);
            // 设置填充色
            CGContextSetFillColorWithColor(context, data.fillColor.CGColor);
            // 填充
            CGContextFillPath(context);
            
            /*
             绘制扇形区域的边框线
             */
            
            // 设定起始点为圆心
            CGContextMoveToPoint(context, circleCenter.x, circleCenter.y);
            // 添加一个圆弧
            CGContextAddArc(context, circleCenter.x, circleCenter.y, radius, offset, endAngle + offset, 0);
            // 关闭路径
            CGContextClosePath(context);
            // 设置画笔颜色
            CGContextSetStrokeColorWithColor(context, self.borderColor.CGColor);
            // 画线
            CGContextStrokePath(context);
            
            /*
             绘制标题和百分比
             */
            
            // 一半的弧度
            CGFloat halfAngle = offset + endAngle/2;
            // 到圆心的距离
            CGFloat farFromCircleCenter = radius*0.7;
            CGFloat X = circleCenter.x + farFromCircleCenter * cos(halfAngle);
            CGFloat Y = circleCenter.y + farFromCircleCenter * sin(halfAngle);
            
            NSMutableParagraphStyle *textStyle = [[NSMutableParagraphStyle alloc] init];//段落样式
            textStyle.alignment = NSTextAlignmentCenter;
            textStyle.lineBreakMode = NSLineBreakByWordWrapping;
            NSDictionary *attrs = @{NSFontAttributeName:self.titleFont,
                                    NSParagraphStyleAttributeName:textStyle,
                                    NSForegroundColorAttributeName:self.titleColor};
            // 拼接 标题 和 百分比
            NSString *text = [NSString stringWithFormat:@"%@\n%.2f%%", data.title, percent*100];
            CGSize titleSize = [text boundingRectWithSize:CGSizeMake(100, 100) options:NSStringDrawingUsesLineFragmentOrigin attributes:attrs context:nil].size;
            CGRect textRect= CGRectMake(X - titleSize.width / 2, Y - titleSize.height / 2, titleSize.width, titleSize.height);
            [text drawInRect:textRect withAttributes:attrs];
            
            // 弧度积累偏移
            offset += endAngle;
        }
    }
}

- (CGFloat)calcTotalValue {
    if (self.dataArray && self.dataArray.count) {
        CGFloat sum = 0;
        for (PieChartData *data in self.dataArray) {
            sum += data.value;
        }
        return sum;
    }
    return 0;
}

说一下思路:

原作《Cocoa-Charts》中绘制圆饼图的代码中有很多不必要的代码我全部给删掉了。比如填充时不需要设定线宽也不需要设定画笔颜色,或者填充时不需要闭合路径,因为填充时自动闭合路径,写了也白写。

首先,画圆弧的起始弧度定在了(-π/2),请看下图来感受一下(-π/2)在哪:

计算全部分支的数值总和,然后计算每个分支的数值在总和中的占比,这个比例就是此分支在整个圆中要占的比例,用这个比例值乘以 2π 来确定它的弧度大小。

先填充扇形再画扇形边框线,这些步骤都很简单。重点是标题的位置,要怎么放才会真的居中?

原作中的标题位置有点尴尬,并没有做到真正居中,让我这个强迫症患者看着很是难受。所以在标题位置这里重新做了一下优化。

我画了一个示意图,我理想中的标题位置如图所示:


绿色虚线是这一分支圆弧的一半弧度,即该圆弧的中心弧度线

CGFloat halfAngle = offset + endAngle/2;

红色实线的长度是到圆心 70%半径 的距离

CGFloat farFromCircleCenter = radius*0.7;

确定了长度和弧度,就有了我们要的点,即文字的中心点,图中的黑色圆点

两条蓝色实线和红色实线形成了直角三角形,就是该中心点用来计算XY坐标的三角函数示意

文字标题的中心点 在该扇形中心弧度线上 距离圆心 70%半径 的位置

CGFloat X = circleCenter.x + farFromCircleCenter * cos(halfAngle);
CGFloat Y = circleCenter.y + farFromCircleCenter * sin(halfAngle);


这样文字的位置可以根据代码 radius*0.7 中的比例来设置文字距离圆心的距离。

圆饼图画完喽!!!




环形图

环形图要继承自圆饼图,因为两者基本是一个思路,用的属性和数据都一样。没什么好说的,看懂圆饼图就明白这个是怎么来的,不比比,直接上代码

#import "PieChart.h"

@interface AnnulusChart : PieChart

/**
 * 圆环宽度占半径的多少比例
 *
 * 取值范围 0-1
 */
@property (nonatomic, assign) CGFloat annulusWidthPercentToRadius;

@end
#import "AnnulusChart.h"
#import "PieChartData.h"

#define PI 3.141592653f

@implementation AnnulusChart

- (void)initProperty {
    [super initProperty];
    self.annulusWidthPercentToRadius = 0.3;
}

- (void)drawRect:(CGRect)rect {
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetAllowsAntialiasing(context, YES);
    CGContextSetStrokeColorWithColor(context, self.borderColor.CGColor);
    
    if (self.dataArray && self.dataArray.count && self.annulusWidthPercentToRadius > 0 && self.annulusWidthPercentToRadius < 1) {
        
        // 数据总和
        CGFloat total = [self calcTotalValue];
        
        // 起始位置弧度
        CGFloat offset = PI * -0.5;
        // 半径和圆心
        CGFloat radius = MIN(rect.size.width, rect.size.height) / 2;
        CGPoint circleCenter = CGPointMake(rect.size.width/2, rect.size.height/2);
        
        // 遍历每一条数据列表
        for (int j = 0; j < [self.dataArray count]; j++) {
            PieChartData *entity = [self.dataArray objectAtIndex:j];
            
            //角度
            CGFloat sweep = entity.value * 2 * PI / total;
            
            // 边线绘制的宽度是圆周内一半外一半
            CGContextSetLineWidth(context, radius * self.annulusWidthPercentToRadius);
            CGContextSetStrokeColorWithColor(context, entity.fillColor.CGColor);
            CGContextAddArc(context, circleCenter.x, circleCenter.y, radius * (1 - self.annulusWidthPercentToRadius / 2), offset, offset + sweep, 0);
            CGContextStrokePath(context);
            
            //调整偏移
            offset = offset + sweep;
            
        }
    }
}

@end

思路还是圆饼图的思路,就是这个 “环” 需要考虑一下。

原作者的做法就是上面代码中的实现方式,圆环用很粗的边框线来实现。这个边框线的宽度就直接用属性 annulusWidthPercentToRadius 乘以半径来定。

边框线如果很宽你就会看到,边框线不是全部绘制在圆周之外,而是一半在圆周内,一半在圆周外。下图白色实线就是圆周线实际的中心位置




还有一种办法就是画一个圆饼图,和前面的圆饼图完全一样,然后在画一个与底色相同的圆覆盖上。这样的好处就是可以有边框线,圆环的分界线会更明显。具体怎么实现就看你的需求了。


区域填充

区域填充就是在画折线图,需要填充的部分把它们的路径闭合进行填充就可以了。

这个填充需要注意两点

要点一

就是在第一篇文章提到的:CGContextFillPath 填充最多可以两条线的路径实现闭合,三条线的路径就失灵了。

什么意思? 请看一条线示意图:


上图给出起点,然后通过三次调用 CGContextAddLineToPoint 画了三条线段,从而连接成一条线的路径,这三个线段连成的线路径是 “一条线的路径”!!!这不是三条线而是一条线!!!

因为只有一条线,没有超过两条线,所以可以进行填充。此时填充这条路径的话会自动闭合起始点和终点,填充的是下图这个区域:




两条线的示意图:

上图中是两条线组成的闭合区域,也可以进行填充,填充的就是上图所示的闭合区域。

如果是下图这样的情况,也会自动闭合:



要点二

两个不相干的封闭路径都可以被一次性正常填充,比如多个矩形


上图这两个路径可以一次性进行填充



说了这么多的前提,那到底我在文章最前面给出的区域填充效果图是怎么填充的呢?




原作者并不是如我所想,使用要点一在两条线之间的区域一次性填充。而是画了无数个闭合的矩形,使用要点二进行一次性多个填充。把代码修改一下就可以再现原作者的思路,我们不填充而是进行画线:
image.png

这就是原作者的思路。

.h文件

#import "LineChart.h"

@interface AreaChart : LineChart

@end

.m 文件

#import "AreaChart.h"
#import "LineData.h"
#import "LinePointData.h"

@implementation AreaChart



- (void)drawData:(CGRect)rect {
    
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetLineWidth(context, 1);
    CGContextSetAllowsAntialiasing(context, YES);
    
    if (self.linesArray != NULL) {
        // 经线间距
        CGFloat longitudeSpacing = 0;
        // 坐标
        __block CGFloat valueX = 0;
        __block CGFloat lastX = 0;
        __block CGFloat lastY = 0;
        // 逐条绘制
        for (LineData *line in self.linesArray) {
            if (!line || !line.linePointsDataArray || line.linePointsDataArray.count < 2) continue;
            // 配置线条
            CGContextSetStrokeColorWithColor(context, line.lineColor.CGColor);
            CGContextSetLineWidth(context, line.lineWidth);

            longitudeSpacing = (rect.size.width - self.axisMarginLeft - self.axisMarginRight) / (line.linePointsDataArray.count - 1);

            valueX = self.axisMarginLeft;

            [line.linePointsDataArray enumerateObjectsUsingBlock:^(LinePointData * _Nonnull point, NSUInteger idx, BOOL * _Nonnull stop) {
                // 计算点的Y坐标
                CGFloat valueY = (1 - (point.valueForY.floatValue - self.minValue) / (self.maxValue - self.minValue)) * (rect.size.height - self.axisMarginTop - self.axisMarginBottom) + self.axisMarginTop;
                // 第一个初始点不画线
                if (idx == 0) {
                    CGContextMoveToPoint(context, valueX, valueY);
                    lastY = valueY;
                }
                else {
                    CGContextAddLineToPoint(context, valueX, valueY);
                    lastY = valueY;
                }
                // X坐标移动
                valueX = valueX + longitudeSpacing;
            }];
            // 绘制路径
            CGContextStrokePath(context);
        }
        
        LineData *line1 = [self.linesArray objectAtIndex:0];
        LineData *line2 = [self.linesArray objectAtIndex:1];


        if (line1 != NULL && line2 != NULL) {
            //设置线条颜色
            CGContextSetStrokeColorWithColor(context, [UIColor greenColor].CGColor);
            //获取线条数据
            NSArray *line1Points = line1.linePointsDataArray;
            NSArray *line2Points = line2.linePointsDataArray;

            // 点线距离
            CGFloat lineLength = ((rect.size.width - self.axisMarginLeft - self.axisMarginRight) / ([line1.linePointsDataArray count] - 1));
            //起始点
            valueX = super.axisMarginLeft;
            //遍历并绘制线条
            for (int j = 0; j < [line1Points count]; j++) {
                LinePointData *line1Point = [line1Points objectAtIndex:j];
                LinePointData *line2Point = [line2Points objectAtIndex:j];

                //获取终点Y坐标
                CGFloat valueY1 = (1 - (line1Point.valueForY.floatValue - self.minValue) / (self.maxValue - self.minValue)) * (rect.size.height - self.axisMarginTop - self.axisMarginBottom) + self.axisMarginTop;
                CGFloat valueY2 = (1 - (line2Point.valueForY.floatValue - self.minValue) / (self.maxValue - self.minValue)) * (rect.size.height - self.axisMarginTop - self.axisMarginBottom) + self.axisMarginTop;

                //绘制线条路径
                if (j == 0) {
                    CGContextMoveToPoint(context, valueX, valueY1);
                    CGContextAddLineToPoint(context, valueX, valueY2);
                    CGContextMoveToPoint(context, valueX, valueY1);
                } else {
                    CGContextAddLineToPoint(context, valueX, valueY1);
                    CGContextAddLineToPoint(context, valueX, valueY2);
                    CGContextAddLineToPoint(context, lastX, lastY);

                    CGContextMoveToPoint(context, valueX, valueY1);
                }
                lastX = valueX;
                lastY = valueY2;
                //X位移
                valueX = valueX + lineLength;
            }
            CGContextClosePath(context);
            CGContextSetAlpha(context, 0.5);
            CGContextSetFillColorWithColor(context, [UIColor yellowColor].CGColor);
            CGContextFillPath(context);
//            CGContextSetStrokeColorWithColor(context, [UIColor blackColor].CGColor);
//            CGContextStrokePath(context);
        }
        
    }
}

- (void)initAxisX {
    [super initAxisX];
}

- (void)initAxisY {
    [super initAxisY];
}

@end



我们还可以使用 要点一 来填充,左右两条竖线和第一条线连接在一起,第二条线和第一条线封闭。


Github示例源码

链接地址:CoreGraphicsDrawChart

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351

推荐阅读更多精彩内容