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

雷達圖最終效果
步驟 + 思路
- 繪制背景(蜘蛛網(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)圖繪制效果
繪制完成后,開始著手分類屬性名稱的繪制,這里可以使用
UILabel或CATextLayer,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;
}

雷達圖底層繪制效果
- 根據(jù)數(shù)據(jù)繪制數(shù)據(jù)圖層
數(shù)據(jù)無非就是0-100、0-1或是100-1000等之間的數(shù)值,并不確定,反觀雷達圖中心點肯定就是最小值,邊框就是最大值,所以我們需要確定圖表中的minValue和maxValue,每個圖層都有它的名稱,線條顏色,填充顏色和數(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地址