ZHLineChartView.m 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  1. //
  2. // ZHLineChartView.m
  3. // ZHLineChart
  4. //
  5. // Created by 周亚楠 on 2020/3/1.
  6. // Copyright © 2020 Zhou. All rights reserved.
  7. //
  8. #import "ZHLineChartView.h"
  9. #import <objc/runtime.h>
  10. @interface ZHLineChartView ()
  11. @property (nonatomic, strong) NSMutableArray *pointArr;
  12. @end
  13. @implementation ZHLineChartView
  14. - (instancetype)initWithFrame:(CGRect)frame
  15. {
  16. self = [super initWithFrame:frame];
  17. if (self) {
  18. self.backgroundColor = [UIColor whiteColor];
  19. self.circleRadius = 3.f;
  20. self.lineWidth = 1.5f;
  21. self.horizontalLineWidth = 0.5f;
  22. self.horizontalBottomLineWidth = 1.f;
  23. self.scaleOffset = 0.f;
  24. self.bottomOffset = 20.f;
  25. self.lineToLeftOffset = 5;
  26. self.angle = M_PI * 1.75;
  27. self.textFontSize = 10;
  28. self.edge = UIEdgeInsetsMake(25, 5, 40, 15);
  29. self.circleStrokeColor = KLineColor;
  30. self.circleFillColor = [UIColor whiteColor];
  31. self.textColor = KTextColor;
  32. self.dataTextColor = KLineColor;
  33. self.lineColor = KLineColor;
  34. self.horizontalLineColor = KHorizontalLineColor;
  35. self.horizontalBottomLineColor = KHorizontalBottomLineColor;
  36. self.addCurve = YES;
  37. self.toCenter = YES;
  38. self.supplement = NO;
  39. self.showLineData = YES;
  40. self.showColorGradient = YES;
  41. self.colorArr = [NSArray arrayWithObjects:(id)[[self.lineColor colorWithAlphaComponent:0.4] CGColor],(id)[[[UIColor whiteColor] colorWithAlphaComponent:0.1] CGColor], nil];
  42. }
  43. return self;
  44. }
  45. /**
  46. * 渲染折线图
  47. */
  48. - (void)drawLineChart
  49. {
  50. NSMutableArray *pointArr = [NSMutableArray array];
  51. CGFloat labelHeight = self.textFontSize;
  52. NSInteger numSpace = (self.max.integerValue - self.min.integerValue) / self.splitCount;
  53. CGFloat spaceY = (self.frame.size.height - self.edge.top - self.edge.bottom - ((self.splitCount + 1) * labelHeight)) / self.splitCount;
  54. CGFloat minMidY = 0.f;
  55. CGFloat maxMidY;
  56. if (self.leftTextWidth <= 0) {
  57. self.leftTextWidth = [self getTextWidth:[NSString stringWithFormat:@"%ld",self.max.integerValue] fontSize:self.textFontSize];
  58. }
  59. for (int i = 0; i < self.splitCount + 1; i ++) {
  60. //创建纵轴文本
  61. UILabel *leftLabel = [self createLabelWithTextColor:self.textColor textAlignment:NSTextAlignmentRight];
  62. leftLabel.frame = CGRectMake(self.edge.left, self.leftTextWidth + (spaceY + labelHeight) * i, self.leftTextWidth, labelHeight);
  63. NSInteger leftNum = self.max.integerValue - numSpace * i;
  64. if (i == self.splitCount) {
  65. leftNum = self.min.integerValue;
  66. //画竖线
  67. UIBezierPath *linePath = [UIBezierPath bezierPath];
  68. // 起点
  69. [linePath moveToPoint:CGPointMake(CGRectGetMaxX(leftLabel.frame) + self.lineToLeftOffset, CGRectGetMinY(CGRectMake(self.edge.left, self.leftTextWidth + (spaceY + labelHeight) * 0, self.leftTextWidth, labelHeight)))];
  70. // 终点
  71. [linePath addLineToPoint:CGPointMake(CGRectGetMaxX(leftLabel.frame) + self.lineToLeftOffset, CGRectGetMaxY(leftLabel.frame))];
  72. CAShapeLayer *lineLayer = [CAShapeLayer layer];
  73. lineLayer.lineWidth = self.horizontalBottomLineWidth;
  74. lineLayer.strokeColor = self.horizontalBottomLineColor.CGColor;
  75. lineLayer.path = linePath.CGPath;
  76. [self.layer addSublayer:lineLayer];
  77. }
  78. leftLabel.text = [NSString stringWithFormat:@"%ld",leftNum];
  79. [self addSubview:leftLabel];
  80. if (!i) {
  81. minMidY = CGRectGetMidY(leftLabel.frame);
  82. }
  83. UIBezierPath *linePath = [UIBezierPath bezierPath];
  84. CGFloat minX = CGRectGetMaxX(leftLabel.frame) + self.lineToLeftOffset;
  85. CGFloat maxX = CGRectGetMaxX(self.frame) - self.edge.right;
  86. [linePath moveToPoint:CGPointMake(minX, CGRectGetMidY(leftLabel.frame))];
  87. [linePath addLineToPoint:CGPointMake(maxX, CGRectGetMidY(leftLabel.frame))];
  88. CAShapeLayer *hLineLayer = [CAShapeLayer layer];
  89. if (i == self.splitCount) {
  90. hLineLayer.strokeColor = self.horizontalBottomLineColor.CGColor;
  91. hLineLayer.lineWidth = self.horizontalBottomLineWidth;
  92. CGFloat spaceX = (maxX - minX) / (self.horizontalDataArr.count - (self.toCenter ? 0 : 1));
  93. maxMidY = CGRectGetMidY(leftLabel.frame);
  94. //创建刻度
  95. UIBezierPath *bezierPath = [UIBezierPath bezierPath];
  96. NSInteger count = self.horizontalDataArr.count;
  97. if (self.toCenter) {
  98. count = self.horizontalDataArr.count + 1;
  99. }
  100. for (int j = 0 ; j < count; j ++) {
  101. [bezierPath moveToPoint:CGPointMake(minX + spaceX * j, maxMidY + self.scaleOffset)];
  102. [bezierPath addLineToPoint:CGPointMake(minX + spaceX * j, maxMidY + 2 + self.scaleOffset)];
  103. [linePath appendPath:bezierPath];
  104. }
  105. //创建横轴文本
  106. CGFloat ratio = (maxMidY - minMidY) / (self.max.floatValue - self.min.floatValue);
  107. for (int k = 0; k < self.horizontalDataArr.count; k ++) {
  108. CGFloat midX = minX + (spaceX * k) + (self.toCenter ? spaceX / 2 : 0);
  109. //构造关键点
  110. NSNumber *tempNum = self.lineDataAry[k];
  111. CGFloat y = maxMidY - (tempNum.integerValue - self.min.floatValue) * ratio;
  112. if (self.toCenter && self.supplement && !k) {
  113. NSValue *value = [NSValue valueWithCGPoint:CGPointMake(minX, y)];
  114. [pointArr addObject:value];
  115. }
  116. NSValue *value = [NSValue valueWithCGPoint:CGPointMake(midX, y)];
  117. [pointArr addObject:value];
  118. if (self.toCenter && self.supplement && k == self.lineDataAry.count - 1) {
  119. NSValue *value = [NSValue valueWithCGPoint:CGPointMake(maxX, y)];
  120. [pointArr addObject:value];
  121. }
  122. //底部文本只显示首尾
  123. if (self.isShowHeadTail && (k > 0 && k < self.horizontalDataArr.count - 1)) {
  124. continue;
  125. }
  126. if (![self.horizontalDataArr[k] length]) {
  127. continue;
  128. }
  129. CGFloat bottomLabelWidth = [self getTextWidth:self.horizontalDataArr[k] fontSize:self.textFontSize];
  130. UILabel *bottomLabel = [self createLabelWithTextColor:self.textColor textAlignment:NSTextAlignmentCenter];
  131. bottomLabel.frame = CGRectMake(midX - bottomLabelWidth / 2, maxMidY + self.bottomOffset, bottomLabelWidth, labelHeight);
  132. bottomLabel.text = self.horizontalDataArr[k];
  133. [self addSubview:bottomLabel];
  134. //旋转
  135. bottomLabel.transform = CGAffineTransformMakeRotation(self.angle);
  136. }
  137. //绘制折线
  138. [self drawLineLayerWithPointArr:pointArr maxMidY:maxMidY];
  139. } else {
  140. hLineLayer.strokeColor = self.horizontalLineColor.CGColor;
  141. hLineLayer.lineWidth = self.horizontalLineWidth;
  142. hLineLayer.lineDashPattern = @[@(5),@(5)];//虚线显示
  143. }
  144. if (i == 0) {
  145. CGFloat lineY = CGRectGetMidY(CGRectMake(self.edge.left, self.leftTextWidth + (spaceY + labelHeight) * 0.5, self.leftTextWidth, labelHeight));
  146. UILabel *rightLabel = [[UILabel alloc] qmui_initWithFont:[UIFont systemFontOfSize:11] textColor:RQColorFromHexString(@"#01C18D")];
  147. rightLabel.text = @"及格线";
  148. CGFloat rightTextWidth = [self getTextWidth:@"及格线" fontSize:11];
  149. rightLabel.frame = CGRectMake(CGRectGetMaxX(self.frame) - self.edge.right - 16 - rightTextWidth, lineY - 4 - labelHeight, rightTextWidth, labelHeight);
  150. [self addSubview:rightLabel];
  151. UIBezierPath *linePath0 = [UIBezierPath bezierPath];
  152. CGFloat minX = CGRectGetMaxX(leftLabel.frame) + self.lineToLeftOffset;
  153. CGFloat maxX = CGRectGetMaxX(self.frame) - self.edge.right;
  154. [linePath0 moveToPoint:CGPointMake(minX, lineY)];
  155. [linePath0 addLineToPoint:CGPointMake(maxX, lineY)];
  156. CAShapeLayer *hLineLayer0 = [CAShapeLayer layer];
  157. hLineLayer0.strokeColor = self.lineColor.CGColor;
  158. hLineLayer0.lineWidth = self.horizontalLineWidth;
  159. hLineLayer0.lineDashPattern = @[@(5),@(5)];//虚线显示
  160. hLineLayer0.path = linePath0.CGPath;
  161. hLineLayer0.fillColor = [UIColor clearColor].CGColor;
  162. hLineLayer0.lineCap = kCALineCapRound;
  163. hLineLayer0.lineJoin = kCALineJoinRound;
  164. hLineLayer0.contentsScale = [UIScreen mainScreen].scale;
  165. [self.layer addSublayer:hLineLayer0];
  166. }
  167. hLineLayer.path = linePath.CGPath;
  168. hLineLayer.fillColor = [UIColor clearColor].CGColor;
  169. hLineLayer.lineCap = kCALineCapRound;
  170. hLineLayer.lineJoin = kCALineJoinRound;
  171. hLineLayer.contentsScale = [UIScreen mainScreen].scale;
  172. [self.layer addSublayer:hLineLayer];
  173. }
  174. }
  175. /**
  176. * 绘制折线及渐变
  177. */
  178. - (void)drawLineLayerWithPointArr:(NSMutableArray *)pointArr maxMidY:(CGFloat)maxMidY
  179. {
  180. CGPoint startPoint = [[pointArr firstObject] CGPointValue];
  181. CGPoint endPoint = [[pointArr lastObject] CGPointValue];
  182. UIBezierPath *linePath = [UIBezierPath bezierPath];
  183. [linePath moveToPoint:startPoint];
  184. if (self.addCurve) {
  185. [linePath addBezierThroughPoints:pointArr];
  186. } else {
  187. [linePath addNormalBezierThroughPoints:pointArr];
  188. }
  189. CAShapeLayer *lineLayer = [CAShapeLayer layer];
  190. lineLayer.path = linePath.CGPath;
  191. lineLayer.strokeColor = self.lineColor.CGColor;
  192. lineLayer.fillColor = [UIColor clearColor].CGColor;
  193. lineLayer.lineWidth = self.lineWidth;
  194. lineLayer.lineCap = kCALineCapRound;
  195. lineLayer.lineJoin = kCALineJoinRound;
  196. lineLayer.contentsScale = [UIScreen mainScreen].scale;
  197. [self.layer addSublayer:lineLayer];
  198. //颜色渐变
  199. if (self.showColorGradient) {
  200. UIBezierPath *colorPath = [UIBezierPath bezierPath];
  201. colorPath.lineWidth = 1.f;
  202. [colorPath moveToPoint:startPoint];
  203. if (self.addCurve) {
  204. [colorPath addBezierThroughPoints:pointArr];
  205. } else {
  206. [colorPath addNormalBezierThroughPoints:pointArr];
  207. }
  208. [colorPath addLineToPoint:CGPointMake(endPoint.x, maxMidY)];
  209. [colorPath addLineToPoint:CGPointMake(startPoint.x, maxMidY)];
  210. [colorPath addLineToPoint:CGPointMake(startPoint.x, startPoint.y)];
  211. CAShapeLayer *bgLayer = [CAShapeLayer layer];
  212. bgLayer.path = colorPath.CGPath;
  213. bgLayer.frame = self.bounds;
  214. CAGradientLayer *colorLayer = [CAGradientLayer layer];
  215. colorLayer.frame = bgLayer.frame;
  216. colorLayer.mask = bgLayer;
  217. colorLayer.startPoint = CGPointMake(0, 0);
  218. colorLayer.endPoint = CGPointMake(0, 1);
  219. colorLayer.colors = self.colorArr;
  220. [self.layer addSublayer:colorLayer];
  221. }
  222. //绘制关键折线及关键点
  223. [self buildDotWithPointsArr:pointArr];
  224. }
  225. /**
  226. * 绘制关键点及关键点数据展示
  227. */
  228. - (void)buildDotWithPointsArr:(NSMutableArray *)pointsArr
  229. {
  230. for (int i = 0; i < pointsArr.count; i ++) {
  231. if (self.toCenter && self.supplement && (!i || i == pointsArr.count - 1)) {
  232. continue;
  233. }
  234. NSValue *point = pointsArr[i];
  235. //关键点绘制
  236. UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(point.CGPointValue.x, point.CGPointValue.y) radius:self.circleRadius startAngle:0 endAngle:M_PI * 2 clockwise:NO];
  237. CAShapeLayer *circleLayer = [CAShapeLayer layer];
  238. circleLayer.path = path.CGPath;
  239. circleLayer.strokeColor = self.circleStrokeColor.CGColor;
  240. circleLayer.fillColor = self.circleFillColor.CGColor;
  241. circleLayer.lineWidth = self.lineWidth;
  242. circleLayer.lineCap = kCALineCapRound;
  243. circleLayer.lineJoin = kCALineJoinRound;
  244. circleLayer.contentsScale = [UIScreen mainScreen].scale;
  245. [self.layer addSublayer:circleLayer];
  246. //关键点数据
  247. if (self.showLineData) {
  248. NSInteger index = i;
  249. if (self.toCenter && self.supplement) {
  250. index = i - 1;
  251. }
  252. UILabel *numLabel = [self createLabelWithTextColor:self.dataTextColor textAlignment:NSTextAlignmentCenter];
  253. numLabel.text = [NSString stringWithFormat:@"%@",self.lineDataAry[index]];
  254. if (self.dataTextWidth <= 0) {
  255. self.dataTextWidth = [self getTextWidth:numLabel.text fontSize:self.textFontSize];
  256. }
  257. numLabel.frame = CGRectMake(point.CGPointValue.x - self.dataTextWidth / 2, point.CGPointValue.y - 18, self.dataTextWidth, self.textFontSize);
  258. [self addSubview:numLabel];
  259. }
  260. }
  261. }
  262. /**
  263. * label创建
  264. */
  265. - (UILabel *)createLabelWithTextColor:(UIColor *)textColor textAlignment:(NSTextAlignment)textAlignment
  266. {
  267. UILabel *label = [[UILabel alloc] init];
  268. label.textColor = textColor;
  269. label.textAlignment = textAlignment;
  270. label.font = [UIFont systemFontOfSize:self.textFontSize];
  271. return label;
  272. }
  273. /**
  274. * 文本宽度计算(by vitasapple)
  275. */
  276. - (CGFloat)getTextWidth:(NSString*)str fontSize:(CGFloat)fontSize{
  277. NSDictionary *attrs = @{NSFontAttributeName : [UIFont systemFontOfSize:fontSize]};
  278. return [str boundingRectWithSize:CGSizeMake(300, 20) options:NSStringDrawingUsesLineFragmentOrigin attributes:attrs context:nil].size.width + 5;
  279. }
  280. @end
  281. /**
  282. UIBezierPath+ThroughPointsBezier
  283. */
  284. @implementation UIBezierPath (ThroughPointsBezier)
  285. - (void)setContractionFactor:(CGFloat)contractionFactor
  286. {
  287. objc_setAssociatedObject(self, @selector(contractionFactor), @(contractionFactor), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  288. }
  289. - (CGFloat)contractionFactor
  290. {
  291. id contractionFactorAssociatedObject = objc_getAssociatedObject(self, @selector(contractionFactor));
  292. if (!contractionFactorAssociatedObject) {
  293. return 0.7;
  294. }
  295. return [contractionFactorAssociatedObject floatValue];
  296. }
  297. /**
  298. * 正常折线绘制
  299. * 必须将CGPoint结构体包装成NSValue对象并且至少一个点来画折线。
  300. */
  301. - (void)addNormalBezierThroughPoints:(NSArray *)pointArray
  302. {
  303. for (int i = 0; i < pointArray.count; i++) {
  304. NSValue * pointIValue = pointArray[i];
  305. CGPoint pointI = [pointIValue CGPointValue];
  306. [self addLineToPoint:pointI];
  307. }
  308. }
  309. - (void)addBezierThroughPoints:(NSArray *)pointArray
  310. {
  311. NSAssert(pointArray.count > 0, @"You must give at least 1 point for drawing the curve.");
  312. if (pointArray.count < 3) {
  313. switch (pointArray.count) {
  314. case 1:
  315. {
  316. NSValue * point0Value = pointArray[0];
  317. CGPoint point0 = [point0Value CGPointValue];
  318. [self addLineToPoint:point0];
  319. }
  320. break;
  321. case 2:
  322. {
  323. NSValue * point0Value = pointArray[0];
  324. CGPoint point0 = [point0Value CGPointValue];
  325. NSValue * point1Value = pointArray[1];
  326. CGPoint point1 = [point1Value CGPointValue];
  327. [self addQuadCurveToPoint:point1 controlPoint:ControlPointForTheBezierCanThrough3Point(self.currentPoint, point0, point1)];
  328. }
  329. break;
  330. default:
  331. break;
  332. }
  333. }
  334. CGPoint previousPoint = CGPointZero;
  335. CGPoint previousCenterPoint = CGPointZero;
  336. CGPoint centerPoint = CGPointZero;
  337. CGFloat centerPointDistance = 0;
  338. CGFloat obliqueAngle = 0;
  339. CGPoint previousControlPoint1 = CGPointZero;
  340. CGPoint previousControlPoint2 = CGPointZero;
  341. CGPoint controlPoint1 = CGPointZero;
  342. previousPoint = self.currentPoint;
  343. for (int i = 0; i < pointArray.count; i++) {
  344. NSValue * pointIValue = pointArray[i];
  345. CGPoint pointI = [pointIValue CGPointValue];
  346. if (i > 0) {
  347. previousCenterPoint = CenterPointOf(self.currentPoint, previousPoint);
  348. centerPoint = CenterPointOf(previousPoint, pointI);
  349. centerPointDistance = DistanceBetweenPoint(previousCenterPoint, centerPoint);
  350. obliqueAngle = ObliqueAngleOfStraightThrough(centerPoint, previousCenterPoint);
  351. previousControlPoint2 = CGPointMake(previousPoint.x - 0.5 * self.contractionFactor * centerPointDistance * cos(obliqueAngle), previousPoint.y - 0.5 * self.contractionFactor * centerPointDistance * sin(obliqueAngle));
  352. controlPoint1 = CGPointMake(previousPoint.x + 0.5 * self.contractionFactor * centerPointDistance * cos(obliqueAngle), previousPoint.y + 0.5 * self.contractionFactor * centerPointDistance * sin(obliqueAngle));
  353. }
  354. if (i == 1) {
  355. [self addQuadCurveToPoint:previousPoint controlPoint:previousControlPoint2];
  356. }
  357. else if (i > 1 && i < pointArray.count - 1) {
  358. [self addCurveToPoint:previousPoint controlPoint1:previousControlPoint1 controlPoint2:previousControlPoint2];
  359. }
  360. else if (i == pointArray.count - 1) {
  361. [self addCurveToPoint:previousPoint controlPoint1:previousControlPoint1 controlPoint2:previousControlPoint2];
  362. [self addQuadCurveToPoint:pointI controlPoint:controlPoint1];
  363. }
  364. else {
  365. }
  366. previousControlPoint1 = controlPoint1;
  367. previousPoint = pointI;
  368. }
  369. }
  370. CGFloat ObliqueAngleOfStraightThrough(CGPoint point1, CGPoint point2) // [-π/2, 3π/2)
  371. {
  372. CGFloat obliqueRatio = 0;
  373. CGFloat obliqueAngle = 0;
  374. if (point1.x > point2.x) {
  375. obliqueRatio = (point2.y - point1.y) / (point2.x - point1.x);
  376. obliqueAngle = atan(obliqueRatio);
  377. }
  378. else if (point1.x < point2.x) {
  379. obliqueRatio = (point2.y - point1.y) / (point2.x - point1.x);
  380. obliqueAngle = M_PI + atan(obliqueRatio);
  381. }
  382. else if (point2.y - point1.y >= 0) {
  383. obliqueAngle = M_PI/2;
  384. }
  385. else {
  386. obliqueAngle = -M_PI/2;
  387. }
  388. return obliqueAngle;
  389. }
  390. CGPoint ControlPointForTheBezierCanThrough3Point(CGPoint point1, CGPoint point2, CGPoint point3)
  391. {
  392. return CGPointMake(2 * point2.x - (point1.x + point3.x) / 2, 2 * point2.y - (point1.y + point3.y) / 2);
  393. }
  394. CGFloat DistanceBetweenPoint(CGPoint point1, CGPoint point2)
  395. {
  396. return sqrt((point1.x - point2.x) * (point1.x - point2.x) + (point1.y - point2.y) * (point1.y - point2.y));
  397. }
  398. CGPoint CenterPointOf(CGPoint point1, CGPoint point2)
  399. {
  400. return CGPointMake((point1.x + point2.x) / 2, (point1.y + point2.y) / 2);
  401. }
  402. @end