學(xué)習(xí)繪制iOS雷達圖

雷達圖

UIBezierPath + CAShaperLayer繪制,先給展示一張最終的效果圖,然后咱們慢慢來說思路

雷達圖最終效果

步驟 + 思路

  1. 繪制背景(蜘蛛網(wǎng)效果)和 分類屬性名的繪制

外層的多邊形:把雷達圖看成一個圓形,最外層的點都是在圓上的,這樣我們就可以在圓上找多個點,然后連接在一起,去繪制了。


圓形的路徑

首先創(chuàng)建一個RadarChartView,開始著手,最外層邊框線的繪制,先要根據(jù)分類的數(shù)據(jù)才能確定圓上有幾個點。

- (void)fl_drawRadarChartBorderLine {
    CAShapeLayer *layer = [CAShapeLayer layer];
    //線條寬度
    layer.lineWidth = 1.0;
    //線條顏色
    layer.strokeColor = [UIColor darkGrayColor].CGColor;
    //填充顏色
    layer.fillColor = [UIColor clearColor].CGColor;
    
    UIBezierPath *path = [UIBezierPath bezierPath];
    //拿到view的中心點坐標chartCenter,不能用self.center,這個獲取的是view在父視圖中的坐標
    CGPoint chartCenter = CGPointMake(self.bounds.size.width / 2, self.bounds.size.height / 2);
    //假數(shù)據(jù)
    NSArray *dataArray = @[@"Objective-C",@"Swift",@"Python",@"Java",@"C",@"c++"];
    //圓半徑
    CGFloat radius = MIN(self.bounds.size.width, self.bounds.size.height) / 2 - 20;
    //子角度
    //1/360 的角度
    NSInteger subAngle = 360 / dataArray.count;
    
    for (NSInteger i = 0; i < dataArray.count; i ++) {
        //角度
        NSInteger angle = i * subAngle;
        //弧度  角度轉(zhuǎn)弧度
        CGFloat radian = (M_PI * (angle) / 180.0);
        //弧度轉(zhuǎn)坐標
        CGFloat x = chartCenter.x + sinf(radian) * radius;
        CGFloat y = chartCenter.y - cosf(radian) * radius;
        CGPoint point = CGPointMake(x, y);
        if (i == 0) {
            //第一個點
            [path moveToPoint:point];
        } else {
            [path addLineToPoint:point];
        }
    }
    //將路徑閉合,即將起點和終點連起
    [path closePath];
    layer.path = path.CGPath;
    [self.layer addSublayer:layer];
}
//用drawRect開始繪制
- (void)drawRect:(CGRect)rect {
    // Drawing code
    [self fl_drawRadarChartBorderLine];
}
邊框線條繪制效果

雷達圖的雛形就已經(jīng)繪制好了,現(xiàn)在開始繪制每一層的線條

- (void)fl_drawRadarChartBorderBackgroundLine {
    CAShapeLayer *layer = [CAShapeLayer layer];
    //線條寬度
    layer.lineWidth = 1.0;
    //線條顏色
    layer.strokeColor = [UIColor darkGrayColor].CGColor;
    //填充顏色
    layer.fillColor = [UIColor clearColor].CGColor;
    
    UIBezierPath *path = [UIBezierPath bezierPath];
    //拿到view的中心點坐標chartCenter,不能用self.center,這個獲取的是view在父視圖中的坐標
    CGPoint chartCenter = CGPointMake(self.bounds.size.width / 2, self.bounds.size.height / 2);
    //假數(shù)據(jù)
    NSArray *dataArray = @[@"Objective-C",@"Swift",@"Python",@"Java",@"C",@"c++"];
    //圓半徑
    CGFloat radius = MIN(self.bounds.size.width, self.bounds.size.height) / 2 - 20;
    //子角度
    //1/360 的角度
    NSInteger subAngle = 360 / dataArray.count;
    //每條線的間隔
    CGFloat lineInterval = 20.0;
    
    //雷達圖間隔可以畫的次數(shù),向上取整
    NSInteger lines = ceilf(radius / lineInterval);
    //保存最外層的坐標點,可用于繪制豎直的線條
    NSMutableArray *borderPointArray = [NSMutableArray array];
    //根據(jù)可繪制的線條數(shù)量循環(huán)
    for (NSInteger idx = 0; idx < lines; idx ++) {
        //循環(huán)繪制每一圈的背景線
        NSMutableArray<NSValue *> *pointArray = [NSMutableArray array];
        for (NSInteger i = 0; i < dataArray.count; i ++) {
            //角度
            NSInteger angle = i * subAngle;
            //弧度  角度轉(zhuǎn)弧度
            CGFloat radian = (M_PI * (angle) / 180.0);
            //弧度轉(zhuǎn)坐標
            CGFloat x = chartCenter.x + sinf(radian) * radius;
            CGFloat y = chartCenter.y - cosf(radian) * radius;
            CGPoint point = CGPointMake(x, y);
            [pointArray addObject:[NSValue valueWithCGPoint:point]];
            if (i == 0) {
                //第一個點
                [path moveToPoint:point];
            } else {
                [path addLineToPoint:point];
            }
        }
        //將路徑閉合,即將起點和終點連起
        [path closePath];
        //減去線條間隔
        radius -= lineInterval;
        if (idx == 0) {
            //獲取最外層的坐標數(shù)組
            [borderPointArray setArray:pointArray];
        }
    }
    //豎向直線
    for (NSValue *boardValue in borderPointArray) {
        CGPoint boardPoint = boardValue.CGPointValue;
        //連接最外層坐標點和雷達圖中心點
        [path moveToPoint:boardPoint];
        [path addLineToPoint:chartCenter];
    }
    
    layer.path = path.CGPath;
    [self.layer addSublayer:layer];
}

蜘蛛網(wǎng)圖繪制效果

繪制完成后,開始著手分類屬性名稱的繪制,這里可以使用UILabelCATextLayer,UILabel就不用講太多了,既然是圖表,還是選擇CATextLayer

最外層的坐標點數(shù)組,這里就還可以用來計算每個分類屬性文字的位置。計算文字的大小可以用系統(tǒng)的

- (CGRect)boundingRectWithSize:(CGSize)size options:(NSStringDrawingOptions)options attributes:(nullable NSDictionary<NSAttributedStringKey, id> *)attributes context:(nullable NSStringDrawingContext *)context NS_AVAILABLE(10_11, 7_0);

或是

- (CGRect)boundingRectWithSize:(CGSize)size options:(NSStringDrawingOptions)options context:(nullable NSStringDrawingContext *)context NS_AVAILABLE(10_11, 6_0);

接下來繪制雷達圖分類屬性文字,根據(jù)邊框上的點來計算,文本應(yīng)該顯示的位置,下面的代碼,進行了封裝,和上面無異


根據(jù)中心點的位計算文本位置
/**
 繪制雷達圖分類屬性文字
 
 @param pointArray 每個角的坐標數(shù)組
 */
- (void)fl_drawRadarChartClassifyTextWithPointArray:(NSArray<NSValue *> *)pointArray {
    for (NSInteger j = 0; j < pointArray.count; j ++) {
        //邊框的位置
        CGPoint borderPoint = pointArray[j].CGPointValue;
        
        CATextLayer *textLayer = nil;
        //文字
        NSString *text = self.classifyDataArray[j];
        //文字大小
        CGSize textSize = [text fl_sizeForFont:self.classifyTextFont];
        //文字的間隔
        CGFloat textInterval = 10;
        
        if (borderPoint.x < self.chartCenter.x && (self.chartCenter.x - borderPoint.x) > 0.05) {
            //判斷是否在圓中心點的左側(cè),多加一個判斷條件,主要用于判斷文本上下兩點的,邊框繪制的點和實際chartCenter.有一點點偏差
            CGRect frame = CGRectMake(borderPoint.x - textSize.width - textInterval, borderPoint.y - textSize.height / 2, textSize.width, textSize.height);
            textLayer = [self fl_getTextLayerWithString:text backgroundColor:[UIColor clearColor] frame:frame];
            
        } else if (borderPoint.x > self.chartCenter.x && (borderPoint.x - self.chartCenter.x) > 0.05) {
            //判斷是否在圓中心點的右側(cè)
            CGRect frame = CGRectMake(borderPoint.x + textInterval, borderPoint.y - textSize.height / 2, textSize.width, textSize.height);
            textLayer = [self fl_getTextLayerWithString:text backgroundColor:[UIColor clearColor] frame:frame];
            
        } else  { //textPoint.x == self.chartCenter.x
            if (borderPoint.y < self.chartCenter.y) {
                //判斷是否在圓中心點的正上方
                CGRect frame = CGRectMake(borderPoint.x - textSize.width / 2, borderPoint.y - textSize.height - textInterval, textSize.width, textSize.height);
                textLayer = [self fl_getTextLayerWithString:text backgroundColor:[UIColor clearColor] frame:frame];
                
            } else {
                //判斷是否在圓中心點的正下方
                CGRect frame = CGRectMake(borderPoint.x - textSize.width / 2, borderPoint.y + textInterval, textSize.width, textSize.height);
                textLayer = [self fl_getTextLayerWithString:text backgroundColor:[UIColor clearColor] frame:frame];
                
            }
        }
        [self.backgroundLineLayer addSublayer:textLayer];
    }
}


/**
 獲取CATextLayer

 @param text 文本
 @param backgroundColor 背景色
 @param frame 位置大小
 @return CATextLayer
 */
- (CATextLayer *)fl_getTextLayerWithString:(NSString *)text backgroundColor:(UIColor *)backgroundColor frame:(CGRect)frame {
    //初始化一個CATextLayer
    CATextLayer *textLayer = [CATextLayer layer];
    //設(shè)置文字frame
    textLayer.frame = frame;
    //設(shè)置文字
    textLayer.string = text;
    //設(shè)置文字大小
    textLayer.fontSize = self.classifyTextFont.pointSize;
    //設(shè)置文字顏色
    textLayer.foregroundColor = self.classifyTextColor.CGColor;
    //設(shè)置背景顏色
    textLayer.backgroundColor = backgroundColor.CGColor;
    //設(shè)置對齊方式
    textLayer.alignmentMode = kCAAlignmentCenter;
    //設(shè)置分辨率
    textLayer.contentsScale = [UIScreen mainScreen].scale;
    return textLayer;
}
雷達圖底層繪制效果
  1. 根據(jù)數(shù)據(jù)繪制數(shù)據(jù)圖層
    數(shù)據(jù)無非就是0-100、0-1或是100-1000等之間的數(shù)值,并不確定,反觀雷達圖中心點肯定就是最小值,邊框就是最大值,所以我們需要確定圖表中的minValuemaxValue,每個圖層都有它的名稱,線條顏色,填充顏色和數(shù)值,數(shù)值的數(shù)量基本是和分類屬性數(shù)量是一致的,所以我們寫了個FLRadarChartModel,定義了以下四個個屬性。
/**
 數(shù)值數(shù)組
 */
@property (nonatomic, strong) NSArray<NSNumber *> *valueArray;

/**
 名稱
 */
@property (nonatomic, strong) NSString *name;

/**
 繪制顏色
 */
@property (nonatomic, strong) UIColor *strokeColor;

/**
 填充顏色
 */
@property (nonatomic, strong) UIColor *fillColor;

然后就可以根據(jù)我們自己定義的model來進行多層繪制。初始化三個假數(shù)據(jù)

FLRadarChartModel *model_1 = [[FLRadarChartModel alloc]init];
model_1.name = @"平均能力";
model_1.valueArray = @[@50,@100,@80,@35,@10,@65];
model_1.strokeColor = [UIColor fl_colorWithHexString:@"00A8FF"];
model_1.fillColor = [UIColor fl_colorWithHexString:@"00A8FF" alpha:0.8];

FLRadarChartModel *model_2 = [[FLRadarChartModel alloc]init];
model_2.name = @"個人能力";
model_2.valueArray = @[@60,@50,@30,@100,@90,@75,];
model_2.strokeColor = [UIColor fl_colorWithHexString:@"FED700"];
model_2.fillColor = [UIColor fl_colorWithHexString:@"FED700" alpha:0.8];

FLRadarChartModel *model_3 = [[FLRadarChartModel alloc]init];
model_3.name = @"屌絲能力";
model_3.valueArray = @[@20,@30,@40,@50,@60,@25,];
model_3.strokeColor = [UIColor fl_colorWithHexString:@"FFC0CB"];
model_3.fillColor = [UIColor fl_colorWithHexString:@"FFC0CB" alpha:0.8];
        
self.dataArray = @[model_1, model_2, model_3];
/**
 繪制雷達圖
 */
- (void)fl_drawRadarChartWithValue {
    for (FLRadarChartModel *dataModel in self.dataArray) {
        
        CAShapeLayer *layer = [CAShapeLayer layer];
        layer.lineWidth = self.radarChartLineWidth;
        layer.strokeColor = dataModel.strokeColor.CGColor;
        layer.fillColor = dataModel.fillColor.CGColor;
        UIBezierPath *bezierPath = [UIBezierPath bezierPath];
        
        //不能使用forin遍歷 如果數(shù)組中存在相同數(shù)據(jù) indexOfObject 獲取的 index 是錯誤的
        for (NSInteger i = 0; i < dataModel.valueArray.count; i ++) {
            NSNumber *value = dataModel.valueArray[i];
            CGFloat numeric = value.floatValue;
            
            //是否允許數(shù)據(jù)溢出
            if (!self.allowOverflow) {
                //判斷當前值是否超過最大最小值
                if (value.floatValue > self.maxValue) {
                    numeric = MIN(value.floatValue, self.maxValue);
                } else if (value.floatValue < self.minValue) {
                    numeric = MAX(value.floatValue, self.minValue);
                }
            }
            
            NSInteger subAngle = 360 / self.classifyDataArray.count;
            
            //error:NSInteger index = [dataModel.valueArray indexOfObject:value];
            
            NSInteger angle = i * subAngle;
            CGFloat radian = kDegreesToRadian(angle);
            //每個數(shù)值的最小半徑
            CGFloat minValueRadius = (self.maxValue - self.minValue) / self.chartRadius;
            
            CGFloat x = self.chartCenter.x + sinf(radian) * (numeric / minValueRadius);
            CGFloat y = self.chartCenter.y - cosf(radian) * (numeric / minValueRadius);
            
            CGPoint valuePoint = CGPointMake(x, y);
            if (i == 0) {
                [bezierPath moveToPoint:valuePoint];
            } else {
                [bezierPath addLineToPoint:valuePoint];
            }
        }
        
        [bezierPath closePath];
        layer.path = bezierPath.CGPath;
        [self.valueLayer addSublayer:layer];
    }
    [self.layer addSublayer:self.valueLayer];
}

已經(jīng)繪制好數(shù)據(jù)圖層了,就還剩圖層顏色和名稱的描述,我將顏色和文字描述定位在view的右下角。顏色圓形用CAShapeLayer,文字用CATextLayer。

/**
 繪制顏色和文字描述
 */
- (void)fl_drawRadarChartWithColorDescribe {
    for (FLRadarChartModel *model in self.dataArray) {
        NSInteger index = [self.dataArray indexOfObject:model];
        CGSize textSize = [model.name fl_sizeForFont:self.classifyTextFont];
        CGFloat textInterval = 5;
        
        //計算文字的繪制位置
        CGRect textFrame = CGRectMake(self.fl_width - textSize.width - 3 * textInterval, self.fl_height - (index + 1) * textSize.height - 2 * (index + 1) * textInterval, textSize.width, textSize.height);
        //計算顏色圓形的繪制位置
        CGRect colorCircleFrame = CGRectMake(textFrame.origin.x - textInterval - textSize.height, textFrame.origin.y, textSize.height, textSize.height);
        
        CAShapeLayer *colorCircleLayer = [self fl_getColorCircleLayerWithColor:model.strokeColor frame:colorCircleFrame];
        [self.colorDescribeLayer addSublayer:colorCircleLayer];
        
        CATextLayer *colorDescribeLayer = [self fl_getTextLayerWithString:model.name backgroundColor:[UIColor clearColor] frame:textFrame];
        [self.colorDescribeLayer addSublayer:colorDescribeLayer];
    }
    [self.layer addSublayer:self.colorDescribeLayer];
}

- (CAShapeLayer *)fl_getColorCircleLayerWithColor:(UIColor *)color frame:(CGRect)frame {
    CAShapeLayer *colorCircleLayer = [CAShapeLayer layer];
    colorCircleLayer.fillColor = [color CGColor];
    colorCircleLayer.strokeColor = [color CGColor];
    colorCircleLayer.lineCap = kCALineCapRound;
    colorCircleLayer.lineWidth = 1;
    UIBezierPath *colorCircleLayerPath = [UIBezierPath bezierPathWithOvalInRect:frame];
    colorCircleLayer.path = colorCircleLayerPath.CGPath;
    return colorCircleLayer;
}

最終效果如圖

最終效果圖

雷達圖的顯示基本上繪制完成了。希望能對大家有用。
最后附上demo地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容