源码解析--PNChart

壁纸.jpg

前言:

PNChart是一个简单漂亮的iOS图表库,在github上面获得了8000多个star,建议先下载这个库配合本文的阅读。它支持以下图形的绘制:

  • PNCircleChart(环形图)
  • PNLineChart(折线图)
  • PNPieChart(饼图)
  • PNBarChart(柱状图)
  • PNRadarChart(雷达图)
  • PNScatterChart(散点图)

层次结构为:


层次结构图.png

我们开始吧:

PNCircleChart(环形图)

PNCircleChart和其他图不一样,它是直接继承UIView。为了方便讲解,我故意添加了背景色和渐变色。如图:

环形图.png

PNCircleChart主要组成部分:

@property (strong, nonatomic) UICountingLabel *countingLabel;//显示百分比文本
@property (nonatomic) CAShapeLayer *circle;//蓝色部分,部分被渐变绿色覆盖
@property (nonatomic) CAShapeLayer *gradientMask;//上图深绿色的部分
@property (nonatomic) CAShapeLayer *circleBackground;//上图灰色部分
PNCircleChart结构图.png

1.拿到图形的路径:

 UIBezierPath *circlePath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.frame.size.width/2.0f, self.frame.size.height/2.0f)
                                                                  radius:(self.frame.size.height * 0.5) - ([_lineWidth floatValue]/2.0f)
                                                              startAngle:DEGREES_TO_RADIANS(startAngle)
                                                                endAngle:DEGREES_TO_RADIANS(endAngle)
                                                               clockwise:clockwise];

2.添加渐变颜色

 // Add gradient
        self.gradientMask = [CAShapeLayer layer];
        self.gradientMask.fillColor = [[UIColor clearColor] CGColor];
        self.gradientMask.strokeColor = [[UIColor blackColor] CGColor];
        self.gradientMask.lineWidth = _circle.lineWidth;
        self.gradientMask.lineCap = kCALineCapRound;
        CGRect gradientFrame = CGRectMake(0, 0, 2*self.bounds.size.width, 2*self.bounds.size.height);
        self.gradientMask.frame = gradientFrame;
        self.gradientMask.path = _circle.path;

        CAGradientLayer *gradientLayer = [CAGradientLayer layer];
        gradientLayer.startPoint = CGPointMake(0.5,1.0);
        gradientLayer.endPoint = CGPointMake(0.5,0.0);
        gradientLayer.frame = gradientFrame;
        UIColor *endColor = (_strokeColor ? _strokeColor : [UIColor greenColor]);
        NSArray *colors = @[
                            (id)endColor.CGColor,
                            (id)_strokeColorGradientStart.CGColor
                            ];
        gradientLayer.colors = colors;
        //如果不添加,你会发现self.gradientMask 添加在self上了,你可以试试
        [gradientLayer setMask:self.gradientMask];
        
        [_circle addSublayer:gradientLayer];

3.UICountingLabel类主要是来实现数字平滑变化的动画。利用CABasicAnimation来实现layer层的动画。动画的相关内容可以参考这里

PNLineChart(折线图)

折线图.png

1.减去左右黄色边距区域,拿到横轴作图区域

_chartCavanWidth = self.frame.size.width - _chartMarginLeft - _chartMarginRight; 

2.红色区域根据数组确定点横坐标,以及布局相关label。

[self.lineChart setXLabels:@[@"SEP 1",@"SEP 2",@"SEP 3",@"SEP 4",@"SEP 5",@"SEP 6",@"SEP 7"]];

3.拿到纵轴作图区域,布局相关label。

_chartCavanHeight = self.frame.size.height - _chartMarginBottom - _chartMarginTop;
[self.lineChart setYLabels:@[
            @"0 min",
            @"50 min",
            @"100 min",
            @"150 min",
            @"200 min",
            @"250 min",
            @"300 min",
            ]
         ];

4.根据提供的点的大小与刚刚计算的纵轴和横轴的值,计算出点具体的frame。然后根据点与点计算点与点的路径。存储下来。并利用UIBezierPathCAShapeLayer作动画。

self.lineChart.chartData = @[data01, data02];//在setter方法里面去做计算路径的操作
[self.lineChart strokeChart]; //画图

//计算x轴的点
int x = i * _xLabelWidth + _chartMarginLeft + _xLabelWidth / 2.0;
//计算y轴的点
int y = _chartCavanHeight - (innerGrade * _chartCavanHeight) - (_yLabelHeight / 2) + _chartMarginTop;      

5.PNLineChartData是关于PNLineChart的一个非常重要的类,它为PNLineChart提供线条相关的颜色,文本字体,点样式信息,比如:

typedef NS_ENUM(NSUInteger, PNLineChartPointStyle) {
    PNLineChartPointStyleNone = 0, //无
    PNLineChartPointStyleCircle = 1,//圆点
    PNLineChartPointStyleSquare = 3,//正方形点
    PNLineChartPointStyleTriangle = 4//三角形点
};
@property (nonatomic) BOOL showPointLabel; //当PNLineChartPointStyle不为PNLineChartPointStyleNone样式时,决定是否在点上面显示点信息的文本。比如上图绿色的区域。

PNBarChart(柱状图)

PNBarChart柱状图和PNLineChart是极其相似的,只不过在确定坐标系后利用去布局PNBar柱对象,PNBar对象负责每个柱对象的样式,颜色和动画。

柱状图.png

动画的实现:

-(void)addAnimationIfNeededWithProgressLine:(UIBezierPath *)progressline
{
    if (self.displayAnimated) {
        CABasicAnimation *pathAnimation = nil;
        
        if (_grade) {
            pathAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
            pathAnimation.fromValue = (id)_chartLine.path;
            pathAnimation.toValue = (id)[progressline CGPath];
            pathAnimation.duration = 0.5f;
            pathAnimation.autoreverses = NO;
            pathAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
            [_chartLine addAnimation:pathAnimation forKey:@"animationKey"];
        }
        else {
            pathAnimation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
            pathAnimation.duration = 1.0;
            pathAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
            pathAnimation.fromValue = @0.0f;
            pathAnimation.toValue = @1.0f;
            [_chartLine addAnimation:pathAnimation forKey:@"strokeEndAnimation"];
        }
        
        [self.gradientMask addAnimation:pathAnimation forKey:@"animationKey"];
    }
}

PNPieChart(饼图)

PNPieChart柱状可以显示当前模块所占百分比,也可以选择选择本身的值。如图(百分比):

饼图.png

1.PNPieChart初始化时会根据比较宽高德大小来以较小的设置直径,以免超出页面。

CGFloat minimal = (CGRectGetWidth(self.bounds) < CGRectGetHeight(self.bounds)) ? CGRectGetWidth(self.bounds) : CGRectGetHeight(self.bounds); //利用MIN宏会比三目运算更可读

2.PNPieChartDataItem类提供包装初始化数据,根据数据提供的itemValue计算出各个item所占比例。我们提供的初始化数据为:

 NSArray *items = @[[PNPieChartDataItem dataItemWithValue:10 color:PNLightGreen],
                           [PNPieChartDataItem dataItemWithValue:20 color:PNFreshGreen description:@"WWDC"],
                           [PNPieChartDataItem dataItemWithValue:40 color:PNDeepGreen description:@"GOOG I/O"],
                           [PNPieChartDataItem dataItemWithValue:30 color:PNMauve description:@"ATR"],
                           ];

3.计算半径,拿到准备动画绘制路径。

//计算半径以及借下来的lineWidth
self.outerCircleRadius = minimal / 2;
self.innerCircleRadius = minimal / 6;
CGFloat radius = _innerCircleRadius + (_outerCircleRadius - _innerCircleRadius) / 2;
CGFloat borderWidth = _outerCircleRadius - _innerCircleRadius;


- (CAShapeLayer *)newCircleLayerWithRadius:(CGFloat)radius
                               borderWidth:(CGFloat)borderWidth
                                 fillColor:(UIColor *)fillColor
                               borderColor:(UIColor *)borderColor
                           startPercentage:(CGFloat)startPercentage
                             endPercentage:(CGFloat)endPercentage{
    CAShapeLayer *circle = [CAShapeLayer layer];
    CGPoint center = CGPointMake(CGRectGetMidX(self.bounds),CGRectGetMidY(self.bounds));
    //从坐标轴-90°出发,拿到路径。
    UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:center
                                                        radius:radius
                                                    startAngle:-M_PI_2
                                                      endAngle:M_PI_2 * 3
                                                     clockwise:YES];
    circle.fillColor   = fillColor.CGColor;
    circle.strokeColor = borderColor.CGColor;
    //根据strokeStart,strokeEnd绘制每个item所占的比例
    circle.strokeStart = startPercentage;
    circle.strokeEnd   = endPercentage;
    circle.lineWidth   = borderWidth;
    circle.path        = path.CGPath;
    
    return circle;
}

4.添加要显示的文本

for (int i = 0; i < _items.count; i++) {
        UILabel *descriptionLabel =  [self descriptionLabelForItemAtIndex:i];
        [_contentView addSubview:descriptionLabel];
        [_descriptionLabels addObject:descriptionLabel];
    }

5.点击事件

//拿到点击的坐标
CGPoint touchLocation = [touch locationInView:_contentView];
//根据点击的点的坐标位置来判断点击的哪块区域,做相应的判断
- (void)didTouchAt:(CGPoint)touchLocation

PNScatterChart(散点图)

散点图.png

1.PNScatterChart根据分别设置X,Y坐标的最小值,和最大值,间断数来确定X,Y的坐标轴。

//比如:x的间距:(100 - 20)/(6 - 1) 
[self.scatterChart setAxisXWithMinimumValue:20 andMaxValue:100 toTicks:6];
        [self.scatterChart setAxisYWithMinimumValue:30 andMaxValue:50 toTicks:5];

2,然后将点绘制在坐标轴中,这个和折线图思路是一致的。

PNRadarChart(雷达图)

雷达图

1.PNRadarChartDataItem类提供包装初始化数据,首先根据item个数决定每个item的角度。

//初始化数据
 NSArray *items = @[[PNRadarChartDataItem dataItemWithValue:3 description:@"Art"],
                           [PNRadarChartDataItem dataItemWithValue:2 description:@"Math"],
                           [PNRadarChartDataItem dataItemWithValue:8 description:@"Sports"],
                           [PNRadarChartDataItem dataItemWithValue:5 description:@"Literature"],
                           [PNRadarChartDataItem dataItemWithValue:4 description:@"Other"],
                           ];

 for (int i=0;i<_chartData.count;i++) {
        PNRadarChartDataItem *item = (PNRadarChartDataItem *)[_chartData objectAtIndex:i];
        [descriptions addObject:item.textDescription];
        [values addObject:[NSNumber numberWithFloat:item.value]];
        CGFloat angleValue = (float)i/(float)[_chartData count]*2*M_PI;
        [angles addObject:[NSNumber numberWithFloat:angleValue]];
    }

2.拿到最大的值(我们这里是8),根据PNRadarChartLabelStyle来计算margin。然后计算出每小格的单位长度_lengthUnit。然后根据angleValue_lengthUnit计算出每个层5个点的坐标放在_pointsToWebArrayArray

  //拿到最大的值
  _maxValue = [self getMaxValueFromArray:values];
    CGFloat margin = 0;
    if (_labelStyle==PNRadarChartLabelStyleCircle) {
        margin = MIN(_centerX , _centerY)*3/10;
    }else if (_labelStyle==PNRadarChartLabelStyleHorizontal) {
        margin = [self getMaxWidthLabelFromArray:descriptions withFontSize:_fontSize];
    }
    CGFloat maxLength = ceil(MIN(_centerX, _centerY) - margin);
    int plotCircles = (_maxValue/_valueDivider);
    if (plotCircles > MAXCIRCLE) {
        NSLog(@"Circle number is higher than max");
        plotCircles = MAXCIRCLE;
        _valueDivider = _maxValue/plotCircles;
    }
    _lengthUnit = maxLength/plotCircles;
    NSArray *lengthArray = [self getLengthArrayWithCircleNum:(int)plotCircles];

    //get all the points and plot
    for (NSNumber *lengthNumber in lengthArray) {
        CGFloat length = [lengthNumber floatValue];
        [_pointsToWebArrayArray addObject:[self getWebPointWithLength:length angleArray:angles]];
    }

3.根据values数组里面的itemValue值和角度来计算点的坐标放在_pointsToPlotArray 里面。

 int section = 0;
    for (id value in values) {
        CGFloat valueFloat = [value floatValue];
        if (valueFloat>_maxValue) {
            NSString *reason = [NSString stringWithFormat:@"Value number is higher than max -value: %f - maxValue: %f",valueFloat,_maxValue];
            @throw [NSException exceptionWithName:NSInvalidArgumentException reason:reason userInfo:nil];
            return;
        }
        
        CGFloat length = valueFloat/_maxValue*maxLength;
        CGFloat angle = [[angles objectAtIndex:section] floatValue];
        CGFloat x = _centerX +length*cos(angle);
        CGFloat y = _centerY +length*sin(angle);
        NSValue* point = [NSValue valueWithCGPoint:CGPointMake(x, y)];
        [_pointsToPlotArray addObject:point];
        section++;
    }

4.根据最大值和角度设置lable"

    [self drawLabelWithMaxLength:maxLength labelArray:descriptions angleArray:angles];

PNChartDelegate

PNGenericChart的点击事件会通过PNChartDelegate协议接口给暴露出来。

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

推荐阅读更多精彩内容