坐标图,经常会在各种各样的App中使用,最常用的一种坐标图就是折线图,根据给定的点绘制出对应的坐标图是最基本的需求。由于本人的项目需要使用折线图,第一反应就是搜索已经存在的解决方案,因为这种需求应该很常见,一定存在不少方案。确实也找到不少,但是没有一个能完全满足需求的,而且一般写的好的都是库很大,包含各种各样的图表,而我这里只需要折线图这一种,也不需要其它功能,于是就决定自己写一个简单的折线图。
基本要求:
1.性能要好,能够快速绘制,改变点时能够时时绘制,有些在数据多的情况下性能实在是差,重新绘制卡顿明显;
2.同一个横坐标可以对应不同的纵坐标(有很多其它的都是根据很坐标计算纵坐标,而这种就不能绘制垂直的线条),这个也是我的项目中需要的
3.能够绘制多条不同颜色的折线
项目地址:https://github.com/huangzhengguo/iOSSamples/tree/master/SwiftTools/SwiftTools/Tools/PlotView
效果图:
大量数据点:
原理:之前一直以为曲线图这种很难做,由于找不到合适的,只能自己动手,才发现若只是实现我这里的需要,还是很简单的。由于我这只是曲线图,所以只用在视图上直接画图即可。由于我这里需求简单,用到了Quartz 2D框架画线即可。
其实我们只要在一个View上面把给定的点使用线连接起来并标记给定的点即可,这样就实现了一条线的绘制。
绘图都是在一个图形上下文上绘制,图形上下文包含绘图系统执行任何绘制命令的绘图参数和所有的特定的设备信息。图形上下文定义了基本的绘图属性,像绘图使用的颜色、绘图剪切的区域、线条宽度以及样式信息、字体信息、合成选项及其他信息。
在iOS的视图图形上下文中绘图
要想iOS应用程序中往屏幕上绘制图形,需要在UIView对象中实现drawRect:方法:执行绘制的方法。这个方法在视图在屏幕上可见或者视图的内容需要更新时调用。在调用自定义的drawRect:方法前,视图会自动配置绘制环境,以便你的代码可以立即开始绘制。作为配置的一部分,视图会为当前的绘制环境创建一个图形上下文。你可以在drawRect:中调用UIGraphicsGetCurrentContext获取图形上下文。
UIKit使用的默认的坐标系统和Quartz使用的不同。在UIKit中,坐标原点在左上角,正的Y值在原点下面。UIView对象会转换Quartz图形上下文以满足UIKit的坐标系统。
下面开始绘制坐标图
首先我们定义一个UIView类,在drawRect中绘制坐标图。我们需要定义一些常用的属性,比如X轴最小最大值,Y轴最小最大值以及线条的颜色等等。
类的初始状态如下:
import UIKit
class PlotView: UIView {
// 顶部边距
var topPadding: CGFloat = 20.0
// 左边距
var leftPadding: CGFloat = 40.0
// 底部边距
var bottomPadding: CGFloat = 30.0
// 右边距
var rightPadding: CGFloat = 20.0
// X轴最小值
var xMinValue: CGFloat = 0.0
// X轴最大值
var xMaxValue: CGFloat = 1439.0
// Y轴最小值
var yMinValue: CGFloat = 0
// Y轴最大值
var yMaxValue: CGFloat = 100.0
// Y轴间隔
var yInterval: CGFloat = 25.0
// X轴间隔
var xInterval: CGFloat = 120.0
// 点的半径
var markRadius: CGFloat = 2.0
// 是否显示Y轴
var yAxisEnable: Bool = true
// 是否显示Y轴标题
var yAxisLabelEnable: Bool = true
// 要绘制的点: 类型为数组中包含数组,每个数组是一条线,数组中的元素为点,包含坐标点的X坐标和Y坐标;可以省略前面的类型指定,swift可以自动推断变量类型
var dataPointArray: [Array<CGPoint>] = [Array<CGPoint>]()
// 线条颜色:数组的简写语法
var lineColorArray = [UIColor]()
// 线条颜色名称
var lineColorTitleArray = [String]()
// 去掉边距后的宽度
var plotWidth: CGFloat?
// 去掉边距后的高度
var plotHeight: CGFloat?
override init(frame: CGRect) {
super.init(frame: frame)
self.plotWidth = self.frame.size.width - self.leftPadding - self.rightPadding
self.plotHeight = self.frame.size.height - self.topPadding - self.bottomPadding
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
}
获取图形上下文并设置绘图使用的默认颜色值:draw方法实现
override func draw(_ rect: CGRect) {
// 获取当前图像上下文
let context = UIGraphicsGetCurrentContext()
context?.setStrokeColor(UIColor.white.cgColor)
context?.setFillColor(UIColor.red.cgColor)
// X轴单位长度
let xUnit = self.plotWidth! / self.xMaxValue
// Y轴单位长度
let yUnit = self.plotHeight! / self.yMaxValue
// 绘制Y轴
if self.yAxisEnable == true {
context?.beginPath()
context?.move(to: CGPoint(x: self.leftPadding, y: self.topPadding))
context?.addLine(to: CGPoint(x: self.leftPadding, y: self.topPadding + self.plotHeight!))
context?.strokePath()
}
// 绘制X轴刻度
for i in 0...((Int)((self.xMaxValue + 1) / self.xInterval) + 1) {
context?.move(to: CGPoint(x: self.leftPadding + CGFloat(i) * xUnit * self.xInterval, y: self.plotHeight! + self.topPadding - 10))
context?.addLine(to: CGPoint(x: self.leftPadding + CGFloat(i) * xUnit * self.xInterval, y: self.plotHeight! + self.topPadding))
context?.strokePath()
}
// 绘制X轴
context?.beginPath()
context?.move(to: CGPoint(x: self.leftPadding, y: self.topPadding + self.plotHeight!))
context?.addLine(to: CGPoint(x: self.leftPadding + self.plotWidth!, y: self.topPadding + self.plotHeight!))
context?.strokePath()
// 根据给定的数据点绘制曲线
context?.beginPath()
for lineIndex in 0...self.dataPointArray.count-1 {
if lineIndex < self.lineColorArray.count {
context?.setStrokeColor(self.lineColorArray[lineIndex].cgColor)
context?.setFillColor(self.lineColorArray[lineIndex].cgColor)
} else {
context?.setStrokeColor(UIColor.white.cgColor)
context?.setFillColor(UIColor.white.cgColor)
}
for pointIndex in 0...self.dataPointArray[lineIndex].count-2 {
// 转换坐标为当前视图的点
let currentPoint = CGPoint(x: self.leftPadding + self.dataPointArray[lineIndex][pointIndex].x * xUnit, y: self.plotHeight! - self.dataPointArray[lineIndex][pointIndex].y * yUnit + self.topPadding)
let nextPoint = CGPoint(x: self.leftPadding + self.dataPointArray[lineIndex][pointIndex + 1].x * xUnit, y: self.plotHeight! - self.dataPointArray[lineIndex][pointIndex + 1].y * yUnit + self.topPadding)
context?.move(to: currentPoint)
// 画线
context?.addLine(to: nextPoint)
context?.strokePath()
// 标记点
context?.addArc(center: currentPoint, radius: markRadius, startAngle: 0.0, endAngle: CGFloat.pi * 2, clockwise: false)
context?.fillPath()
// 如果是最后一段线条,则标记最后一个点
if pointIndex == self.dataPointArray[lineIndex].count-2 {
context?.addArc(center: nextPoint, radius: markRadius, startAngle: 0.0, endAngle: CGFloat.pi * 2, clockwise: false)
context?.fillPath()
}
}
}
}