CoreGraphics绘制图表教程(三)

作者: ChinaChong | 来源:发表于2018-12-07 08:57 被阅读45次

    本篇介绍如何使用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

    相关文章

      网友评论

        本文标题:CoreGraphics绘制图表教程(三)

        本文链接:https://www.haomeiwen.com/subject/pdpgqqtx.html