用Swift玩玩图表

Posted by Kriz on 2017-04-23

最近需要用到图表,翻了一段时间发现了个比较好用的第三方库,由RealmCharts结合而成。前者作为存储结构,后者作为显示介质。

很好用又很好看,总之还挺值得学一下的。

安装

这里只演示通过cocoapods安装的方法。

Xcode新建单个Swift项目,终端敲到根目录,输入

1
pod init

用vim或者别的什么打开生成的Podfile,把要用到的都丢进去。这里做demo就只丢Charts和Realm:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
target 'YOUR_PROJECT_NAME' do
use_frameworks!
pod 'Charts', :git => 'https://github.com/danielgindi/Charts.git', :branch => 'master'
pod 'RealmSwift', '~> 2.0.2'
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['SWIFT_VERSION'] = '3.0'
end
end
end
end

Realm那里,官方的说明文档似乎不建议去掉强制版本号,为了和谐稳定你们这些年轻人就不要更新那么快,不要总想着搞个大新闻。

保存退出安装:

1
pod install

如果报错就更新下pod再装:

1
2
pod repo update
pod install

入门:来画个图!

练手的话首先做个柱状图好了,这里以一个根本没卵用的模拟随时间变化的网站流量柱状图为例。

处理布局

打开根目录下的workspace。Command+B会蹦出来一堆警告,不用管,好像是OC和Swift混编留下的伤痕。(这里如果找到了解决方法再更新)

打开Main.storyboard,拖一个text field用来输入数据,一个button用来响应action,一个view用来更新图表。

pic1

为了一会儿能显示图表,在view的右侧identity栏里,把custom class组下的class设定为BarChartView

同时,为了防止误输入,在text field的attributes栏里把keyboard type设置为number pad

点开assinstant editor(右上角双环),把text field、view和button分别按着ctrl拖进Viewcontroller里,记得将button的connection设置为action。我分别把它们命名为tfValue,barView和btnAddTapped。

顺便把用到的库引好:

1
2
import Charts
import RealmSwift

建立存储结构

建立一个新的swift文件,引入RealmSwift库,创建一个Object类,名为VisitorCount。ObjectRealm里的数据类型,按着alt查看有比较详细的使用规范。

添加两个属性,用来记录日期和访问量:

1
2
dynamic var date = Date()
dynamic var count = Int(0)

创建save方法,便于一会儿添加新数据:

1
2
3
4
5
6
7
8
9
10
func save() {
do {
let realm = try Realm()
try realm.write {
realm.add(self)
}
} catch let error as NSError {
fatalError(error.localizedDescription)
}
}

添加按钮行为

令ViewController的按钮被按下后读取并存储数据:

1
2
3
4
5
6
7
8
@IBAction func btnAddTapped(_ sender: AnyObject) {
if let value = tfValue.text, value != "" {
let visitorCount = VisitorCount()
visitorCount.count = (NumberFormatter().number(from: value)?.intValue)!
visitorCount.save()
tfValue.text = ""
}
}

显示数据图表

在初始化和按钮被按下时,图表应该进行一次刷新。因此,我们在viewDidLoad和button action里加入这个部分,顺便在按钮刷新时把小键盘隐藏掉:

1
2
3
4
5
@IBAction func btnAddTapped(_ sender: AnyObject) {
...
updateChartWithData()
tfValue.resignFirstResponder()
}
1
2
3
4
override func viewDidLoad( {
...
updateChartWithData()
}

更新图表函数的定义:

1
2
3
4
5
6
7
8
9
10
11
func updateChartWithData() {
var dataEntries: [BarChartDataEntry] = []
let visitorCounts = getVisitorCountsFromDatabase()
for i in 0..<visitorCounts.count {
let dataEntry = BarChartDataEntry(x: Double(i), y: Double(visitorCounts[i].count))
dataEntries.append(dataEntry)
}
let chartDataSet = BarChartDataSet(values: dataEntries, label: "Visitor count")
let chartData = BarChartData(dataSet: chartDataSet)
barView.data = chartData
}

其中

1
2
3
4
5
6
7
8
func getVisitorCountsFromDatabase() -> Results<VisitorCount> {
do {
let realm = try Realm()
return realm.objects(VisitorCount.self)
} catch let error as NSError {
fatalError(error.localizedDescription)
}
}

Command+R跑一下,已经有雏形了。(请暂时不要在意那个Clear)

pic2

输入几个数据来测试一下:

pic3

设定横坐标

目前的横坐标被我们设置为Double(i),现在要把它修改成时间格式。

使用代理来规定格式,首先增加一个delegate:

1
2
3
4
5
6
7
class ViewController: UIViewController {
@IBOutlet weak var tfValue: UITextField!
@IBOutlet weak var barView: BarChartView!
weak var axisFormatDelegate: IAxisValueFormatter?
...
}

实现部分直接用viewController完成就好,所以在viewDidLoad加一句:

1
2
3
4
5
override func viewDidLoad() {
super.viewDidLoad()
axisFormatDelegate = self
...
}

随后,我们修改updateChartWithData的横坐标设定部分。因为横坐标需要类型为Double的值,所以先以秒数转换好,最后再利用axisFormatDelegate规范格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func updateChartWithData() {
var dataEntries: [BarChartDataEntry] = []
let visitorCounts = getVisitorCountsFromDatabase()
for i in 0..<visitorCounts.count {
let timeIntervalForDate: TimeInterval = visitorCounts[i].date.timeIntervalSince1970
let dataEntry = BarChartDataEntry(x: Double(timeIntervalForDate), y: Double(visitorCounts[i].count))
dataEntries.append(dataEntry)
}
let chartDataSet = BarChartDataSet(values: dataEntries, label: "Visitor count")
let chartData = BarChartData(dataSet: chartDataSet)
barView.data = chartData
let xaxis = barView.xAxis
xaxis.valueFormatter = axisFormatDelegate
}

IAxisValueFormatter的实现我们放到最最下面来写,利用DateFormatter将时间转换回字符串:

1
2
3
4
5
6
7
8
extension ViewController: IAxisValueFormatter {
func stringForValue(_ value: Double, axis: AxisBase?) -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = “HH:mm.ss”
return dateFormatter.string(from: Date(timeIntervalSince1970: value))
}
}

再跑一次,横坐标就已经变成日期格式了。

pic4

清空数据和视图

清空数据的实现还是很简单的,在storyboard里拉一个button,改名为Clear,在viewController里绑一个变量,起名为btnClearTadded。

pic5

在VisitorCount中建立新方法:

1
2
3
4
5
6
7
8
9
10
func clear() {
do {
let realm = try Realm()
try realm.write {
realm.deleteAll()
}
} catch let error as NSError {
fatalError(error.localizedDescription)
}
}

之后在按钮响应方法中调用就可以了:

1
2
3
4
5
6
@IBAction func btnClearTapped(_ sender: Any) {
let visitorCount = VisitorCount()
visitorCount.clear()
tfValue.resignFirstResponder()
updateChartWithData()
}

Command+R运行,清掉目前的数据。此时会发现一张空白坐标系页面,这是因为我们之前直接规定了barView.data = chartData。如果注释掉viewDidLoad中的updateChartWithData(),就能够看到默认界面了,有一句没有数据时的提示。要修改这个提示,只需要在viewDidLoad里加上

1
2
barView.noDataText = "你的网站没人看:)"
barView.noDataTextColor = .red

啊这爽快的嘲讽感就扑面而来了。

pic6

为了在没有数据的情况下第一时间嘲讽用户,我们修改一下updateChartWithData方法:

1
2
3
4
5
6
7
8
9
10
11
12
func updateChartWithData() {
...
if visitorCounts.count != 0 {
for i in 0..<visitorCounts.count {
...
}
...
xaxis.valueFormatter = axisFormatDelegate
} else {
barView.clear()
}
}

是的,表格视图的清除只需要调用clear方法即可。

进阶:个性化

接下来我们换个战场重新搞事。

在这一部分里,我们以某产品的月度销售量图表(严肃)为例,研究可能会经常用到的属性。

在storyboard里新建一个View Controller,拖一个view进来填满它。这次为了重点突出,就不提供输入了。和之前一样,把它的Class改成BarChartView。

新建cocoa touch class文件,继承自UIViewController,命名为MonthViewController。把刚才新建的View Controller的class改成MonthViewController,把其中的view也一起拖进来,变量名为barChart。

我们来完成一下基础逻辑先:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var months: [String]!
func setChart(dataPoints: [String], values: [Double]) {
var dataEntries: [BarChartDataEntry] = []
for i in 0..<dataPoints.count {
let dataEntry = BarChartDataEntry(x: Double(i), y: values[i])
dataEntries.append(dataEntry)
}
let chartDataSet = BarChartDataSet(values: dataEntries, label: "Units Sold")
let chartData = BarChartData(dataSet: chartDataSet)
barChart.data = chartData
}
override func viewDidLoad() {
super.viewDidLoad()
months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
let unitsSold = [20.0, 4.0, 6.0, 3.0, 12.0, 16.0, 4.0, 18.0, 2.0, 4.0, 5.0, 4.0]
setChart(dataPoints: months, values: unitsSold)
}

跑一下看看效果:

pic7

……等等,右下角有个奇怪的description label,长得就很想让人去掉它:

1
barChart.chartDescription = nil

不同上一个demo粗略的展示,让我们稍微看一下setChart函数里面的重要变量。

chartDataSet用来处理数据集,chartData则可以理解成以某种方式把chartDataSet集合起来,通常这种方式是按数据排列顺序从左至右展示。可以发现Chart3.0中实例化BarChartData传入的参数只有一个BarChartDataSet,对于chartData的横坐标,我们使用xAxis来专门处理。

处理横坐标格式

现在有几个问题需要解决。首先,我不希望横坐标是阿拉伯数字,而是月份的简称,就像我传入的Month数组显示的那样。其次,我想把横坐标的标签放到下面来,如果能把每个月的名字都显示出来就更好了。我们加几条语句,分别来处理这几个问题:

1
2
3
4
let xaxis = barChart.xAxis
xaxis.valueFormatter = IndexAxisValueFormatter(values: dataPoints)
xaxis.labelPosition = XAxis.LabelPosition.bottom
xaxis.labelCount = dataPoints.count

效果还不错:

pic9

xAxis还有更多可以设置的属性,比如标签过长自动换行:

1
xaxis.wordWrapEnabled = true

改字体字号:

1
xaxis.labelFont = UIFont(name: "Georgia", size: 10)!

顺便也可以改纵坐标值的字体字号:

1
chartData.setValueFont(UIFont(name: "Georgia", size: 10)!)

修改图表颜色

一句话的事情:

1
chartDataSet.colors = [UIColor(red: 40/255, green: 43/255, blue: 53/255, alpha: 1)]

可以看到颜色被改掉了:

pic10

除了使用UIColor规定颜色外,还可以使用预设的浮夸配色。比如:

1
chartDataSet.colors = ChartColorTemplates.colorful()

来看看效果:

pic11

除了colorful之外,还有liberty、joyful、pastel和vordiplom可供选择。

如果要改图标的背景底色的话:

1
barChart.backgroundColor = UIColor(red: 189/255, green: 195/255, blue: 199/255, alpha: 1)

动画效果

Charts还是有比较丰富的图表增长效果的。举例说明,想做一个x和y方向在2秒钟之内生长完毕的动画效果,只需要在viewDidLoad里加上:

1
barChart.animate(xAxisDuration: 2, yAxisDuration: 2)

其中x和y可以单独使用。除此之外,还有很多带ease效果动画的animate函数重载,但因为太浮夸了因此请自行体会。

目标线

在图表中标注一条Target线,有的时候会用到。使用方式是:

1
2
let tgLine = ChartLimitLine(limit: 10.0, label: "Target")
barChart.rightAxis.addLimitLine(tgLine)

大概长这样:

pic12

保存图表

这种功能都要特地封装个函数可以说是非常的过分了。

1
barChart.saveToCameraRoll()

拓展:其他图表

接下来看看其他类型的图表。

折线图

首先来写个默认的折线图。开一个新文件,把setChart的内容作简单的修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func setChart(dataPoints: [String], values: [Double]) {
var dataEntries: [ChartDataEntry] = []
for i in 0..<dataPoints.count {
let dataEntry = ChartDataEntry(x: Double(i), y: values[i])
dataEntries.append(dataEntry)
}
let chartDataSet = LineChartDataSet(values: dataEntries, label: "Units Sold")
let chartData = LineChartData(dataSet: chartDataSet)
lineChart.data = chartData
lineChart.chartDescription = nil
let xaxis = lineChart.xAxis
xaxis.valueFormatter = IndexAxisValueFormatter(values: dataPoints)
}

效果如下:

pic13

当然chartDataSet仍然有一堆属性可以用来玩耍,名字都很直白就不一个一个说了:

1
2
3
chartDataSet.colors = ChartColorTemplates.pastel()
chartDataSet.setCircleColors(.brown)
chartDataSet.circleRadius = 5

pic15

有linear、stepped、cubicBezier和horizontalBezier4种曲线模式可以选择。

1
chartDataSet.mode = .cubicBezier

pic14

饼图

换掉setChart下面那部分,这里随机配个色:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let chartDataSet2 = PieChartDataSet(values: dataEntries, label: "Units Sold")
var colors: [UIColor] = []
for _ in 0..<dataPoints.count {
let red = Double(arc4random_uniform(256))
let green = Double(arc4random_uniform(256))
let blue = Double(arc4random_uniform(256))
let color = UIColor(red: CGFloat(red/255), green: CGFloat(green/255), blue: CGFloat(blue/255), alpha: 1)
colors.append(color)
}
chartDataSet2.colors = colors
let chartData2 = PieChartData(dataSet: chartDataSet2)
pieChart.data = chartData2

效果图:

pic16

其他的也大同小异。

总结

用下来感觉还挺强的,不过网上文档是真的少,也没有系统的官方教程,大部分时间只能自己摸索。

最后放几张官图感受一下。

lc

lcc

lccc

bc

bcg

pc

sc

bbc

snc

就很优雅(。

源码戳这里

参考文献

http://cocoadocs.org/docsets/Charts/3.0.2/

https://github.com/danielgindi/Charts/tree/master/ChartsDemo/Classes/Demos

https://medium.com/@skoli/using-realm-and-charts-with-swift-3-in-ios-10-40c42e3838c0

https://github.com/danielgindi/Charts/issues/2280

https://www.appcoda.com/ios-charts-api-tutorial/

http://www.jianshu.com/p/d1c1cefaa35a