iOS - 折线图 Swift 实现

背景:由于公司项目需要绘制曲线图,目前只有用到折线图这一种,一开始并没有自己绘制折线图的想法,只是找了一个第三方的库来用,图一个简单方便。但是找了很久,找到一个勉强能够使用,但是效率太差,数据量大的时候效率非常的差,并不能在修改数据的时候时时的显示出最新的绘制效果。

基本需求:

  • 实现折线图
  • 连接点大小可自定义
  • 可以绘制多条折线
  • 可以给不同折线标记颜色
  • 可以添加图例
  • 实现 X 轴,Y 轴

代码如下:

//
//  SPlotView.swift
////
//  Created by mac min on 2021/1/29.
//  Copyright © 2021 Inledco. All rights reserved.
//

import UIKit

class PlotView: UIView {

    // 顶部边距
    var topMargin: CGFloat = 5.0
    // 左边距
    var leftMargin: CGFloat = 40.0
    // 底部边距
    var bottomMargin: CGFloat = 30.0
    // 右边距
    var rightMargin: CGFloat = 20.0
    // X轴最小值
    var xMinValue: CGFloat = 0.0
    // X轴最大值
    var xMaxValue: CGFloat = 100.0
    // Y轴最小值
    var yMinValue: CGFloat = 0.0
    // Y轴最大值
    var yMaxValue: CGFloat = 100.0
    // Y轴间隔
    var yInterval: CGFloat = 25.0
    // X间隔
    var xInterval: CGFloat = 120.0
    // 是否显示Y轴
    var yAxisEnable: Bool = false
    
    // 曲线图宽度:除去边距
    var plotWidth: CGFloat = 0.0
    // 曲线图高度:除去边距
    var plotHeight: CGFloat = 0.0
    // 标记是否已经添加横坐标 纵坐标 图例等
    var isAddLabel: Bool = false
    
    /**
     * 数据点
     * 格式:
     * 1. 一层数组包含每种颜色的数据
     * 2. 数组中的每个对象包含对应颜色的所有数据
     */
    var dataPointArray: [[CGPoint]]?
    // 线条颜色
    var lineColorArray: [UIColor]?
    // 线条颜色名称
    var lineColorTitleArray: [String]?
    // 预览指示器
    var indicatorLabel: UILabel = UILabel.init(frame: CGRect.zero)
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        
        self.translatesAutoresizingMaskIntoConstraints = false
        self.backgroundColor = .clear
        
        self.plotWidth = frame.size.width - self.leftMargin - self.rightMargin
        self.plotHeight = frame.size.height - self.topMargin - self.bottomMargin
        
        self.isAddLabel = false
        
        self.indicatorLabel.frame = CGRect.init(x: 0.0, y: self.topMargin, width: 1.0, height: self.plotHeight)
        self.indicatorLabel.isHidden = true
        self.indicatorLabel.backgroundColor = .green
        
        self.addSubview(self.indicatorLabel)
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }
    
    // 添加图例
    func addLegend(colorArray: [UIColor], colorTitleArray: [String]) -> Void {
        
        var colorLableWidth = self.leftMargin
        var previousTitleLabel: UILabel?
        for i in 0..<colorArray.count {
            if i != 0 {
                colorLableWidth = colorLableWidth + 20
            }
            
            let label = UILabel.init(frame: CGRect(x: colorLableWidth, y: self.frame.size.height - 10, width: 10, height: 10))
            
            label.translatesAutoresizingMaskIntoConstraints = false
            label.backgroundColor = colorArray[i]
            
            self.addSubview(label)
            
            // 添加约束
            var labelLeadingLayoutConstraint = NSLayoutConstraint(item: label, attribute: .leading, relatedBy: .equal, toItem: self, attribute: .leading, multiplier: 1.0, constant: self.leftMargin)
            let labelBottomLayoutConstraint = NSLayoutConstraint(item: label, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1.0, constant: -8.0)
            let labelHeightLayoutConstraint = NSLayoutConstraint(item: label, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: 10.0)
            let labelWidthLayoutConstraint = NSLayoutConstraint(item: label, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: 10.0)
            if previousTitleLabel != nil {
                labelLeadingLayoutConstraint = NSLayoutConstraint(item: label, attribute: .leading, relatedBy: .equal, toItem: previousTitleLabel, attribute: .trailing, multiplier: 1.0, constant: 8.0)
            }
            
            self.addConstraints([labelLeadingLayoutConstraint, labelBottomLayoutConstraint, labelHeightLayoutConstraint, labelWidthLayoutConstraint])
            
            let titleLabel = UILabel(frame: CGRect(x: 15 + colorLableWidth, y: self.frame.size.height - 10, width: SystemInfo.screenWidth / CGFloat((colorArray.count + 1)), height: 10.0))
            
            titleLabel.translatesAutoresizingMaskIntoConstraints = false
            titleLabel.font = UIFont.ex_systemFontOfSize(fontSize: 8.0)
            titleLabel.text = colorTitleArray[i]
            titleLabel.textColor = .white
            
            self.addSubview(titleLabel)
            
            // 添加约束
            let titleLabelLeadingLayoutConstraint = NSLayoutConstraint(item: titleLabel, attribute: .leading, relatedBy: .equal, toItem: label, attribute: .trailing, multiplier: 1.0, constant: 4.0)
            let titleLabelBottomLayoutConstraint = NSLayoutConstraint(item: titleLabel, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1.0, constant: -8.0)
            
            self.addConstraints([titleLabelLeadingLayoutConstraint, titleLabelBottomLayoutConstraint])
            
            previousTitleLabel = titleLabel
        }
    }
    
    // 刷新视图
    func refreshPlotView() -> Void {
        self.plotWidth = self.frame.size.width - self.leftMargin - self.rightMargin
        self.plotHeight = self.frame.size.height - self.topMargin - self.bottomMargin
        // KMYLOG(@"self.plotHeight = %f", self.frame.size.height - self.topMargin - self.bottomMargin);
        // 是否添加横纵坐标
        if (self.isAddLabel == false && self.plotHeight > 0) {
            self.addXLabel()
            self.addYLabel()
            
            self.isAddLabel = true
        }

        self.setNeedsDisplay()
    }
    
    // MARK: 添加 X 轴刻度
    private func addXLabel() -> Void {
        let xUint = self.plotWidth / (self.xMaxValue - self.xMinValue)
        for i in 0..<GlobalConstant.DAY_SEPARATE_BY_MINUTE / Int(self.xInterval) + 1 {
            if i % 3 == 0 {
                let label = UILabel.init(frame: CGRect.init(x: self.leftMargin + CGFloat(i) * xUint * self.xInterval - 10, y: self.frame.size.height - 38, width: self.frame.size.width / 5, height: 20))
                label.text = String.init(format: "%02d:00", i * 2)
                if i == GlobalConstant.DAY_SEPARATE_BY_MINUTE / Int(self.xInterval) {
                    label.text = "00:00"
                }
                
                label.textColor = .white
                label.textAlignment = .left
                
                self.addSubview(label)
            }
        }
    }
    
    // MARK: 添加 Y 轴刻度
    private func addYLabel() -> Void {
        let yUint = self.plotHeight / (self.yMaxValue - self.yMinValue)
        // 线条数
        let labelCount: Int = Int((self.yMaxValue - self.yMinValue) / self.yInterval + 1)
        for i in 0..<labelCount {
            let label = UILabel.init(frame: CGRect(x: 0, y: yUint * CGFloat(i) * self.yInterval + self.topMargin - 8.0, width: self.leftMargin, height: 16.0))
            
            label.text = String(format: "%2d", 100 - i * 25)
            label.textColor = .white
            label.textAlignment = .center
            
            self.addSubview(label)
        }
    }
    
    // Only override draw() if you perform custom drawing.
    // An empty implementation adversely affects performance during animation.
    override func draw(_ rect: CGRect) {
        if self.dataPointArray == nil {
            return
        }
        
        // 绘制坐标图
        let context = UIGraphicsGetCurrentContext()
        context?.setStrokeColor(UIColor.white.cgColor)
        
        // X轴与Y轴单位长度
        let xUint = self.plotWidth / self.xMaxValue
        let yUint = self.plotHeight / self.yMaxValue
        
        // 绘制Y轴
        if self.yAxisEnable == true {
            context?.beginPath()
            context?.move(to: CGPoint(x: self.leftMargin, y: self.topMargin))
            context?.addLine(to: CGPoint(x: self.leftMargin, y: self.plotHeight + self.topMargin))
            context?.closePath()
            context?.strokePath()
        }
        
        // 绘制X轴刻度
        for i in 0..<GlobalConstant.DAY_SEPARATE_BY_MINUTE / Int(self.xInterval) + 1 {
            context?.beginPath()
            context?.move(to: CGPoint(x: self.leftMargin + CGFloat(i) * xUint * self.xInterval, y: self.plotHeight  + self.topMargin - 10))
            context?.addLine(to: CGPoint(x: self.leftMargin + CGFloat(i) * xUint * self.xInterval, y: self.plotHeight + self.topMargin))
            context?.closePath()
            context?.strokePath()
        }
        
        // 绘制X轴及横线
        let horizonalCount: Int = Int(self.yMaxValue / self.yInterval + 1)
        for i in 0..<horizonalCount {
            context?.beginPath()
            context?.move(to: CGPoint(x: self.leftMargin, y: self.plotHeight / 4.0 * CGFloat(i) + self.topMargin))
            context?.addLine(to: CGPoint(x: self.plotWidth + self.leftMargin, y: self.plotHeight / 4.0 * CGFloat(i) + self.topMargin))
            context?.closePath()
            context?.strokePath()
        }
        
        // 绘制曲线
        context?.setLineWidth(1.5)
        context?.beginPath()
        for i in 0..<self.dataPointArray!.count {
            if self.lineColorArray == nil || i > self.lineColorArray!.count - 1 {
                context?.setStrokeColor(UIColor.white.cgColor)
                context?.setFillColor(UIColor.white.cgColor)
            } else {
                context?.setStrokeColor(self.lineColorArray![i].cgColor)
                context?.setFillColor(self.lineColorArray![i].cgColor)
            }
            
            let lineDataArray = self.dataPointArray![i]
            if lineDataArray.count < 1 {
                return
            }
            
            for j in 0..<lineDataArray.count - 1 {
                let prePoint: CGPoint = lineDataArray[j]
                let nextPoint: CGPoint = lineDataArray[j + 1]
                let preX: CGFloat = prePoint.x * xUint + self.leftMargin
                let preY: CGFloat = self.plotHeight - prePoint.y * yUint + self.topMargin
                let nextX: CGFloat = nextPoint.x * xUint + self.leftMargin
                let nextY: CGFloat = self.plotHeight - nextPoint.y * yUint + self.topMargin
                
                context?.move(to: CGPoint(x: preX, y: preY))
                context?.addLine(to: CGPoint(x: nextX, y: nextY))
                context?.strokePath()
                
                context?.addArc(center: CGPoint(x: preX, y: preY), radius: 3.0, startAngle: 0.0, endAngle: CGFloat.pi * 2, clockwise: true)
                if j == lineDataArray.count - 2 {
                    context?.addArc(center: CGPoint(x: nextX, y: nextY), radius: 3.0, startAngle: 0.0, endAngle: CGFloat.pi, clockwise: true)
                }
                
                context?.fillPath()
            }
        }
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        
        // 使用约束时刷新曲线图
        self.refreshPlotView()
    }

}

 

posted @ 2021-04-23 10:55  sims  阅读(236)  评论(0编辑  收藏  举报