关键字:iOS Charts 图表 swift BarChart 直方图
本文给出两个使用 danielgindi/Charts > GitHub > 的示例,以及在使用过程中遇到的坑。
本人正在开发一个有关时间统计的小应用,其中有两个统计图的功能:一个是日统计图,另一个是周统计图。
最终效果
日统计图是一个直方图,因为用在列表中,尺寸不能太大,所以比较简单,没有网格线,只有右侧的Y轴刻度。X轴表示不同的颜色,Y轴表示时间,显示为 “HH:mm” 格式。
周统计图也是一个直方图,用在单独界面中的,尺寸比较大,可以显示更复杂的数据。这种一个X坐标上堆叠好几个条块的直方图叫 Stacked Bar Chart,也就是一个X值对应多个Y值并画成堆叠的形式。
Charts 使用
安装
用 CocoaPods 安装
pod 'Charts'
使用
如果使用 storyboard 或者 xib,先拖拽一个 UIView,并且设置 Custom Class 为 BarChartView
。BarChartView
表示直方图,如果是其他类型的图,需要使用对应的 ChartView。然后在代码中对 BarChartView 进行设置,基本上所有的设置都是用代码完成的。
如果不做任何配置,只填充数据,也是可以用的,默认会显示比较多的图形元素。下图是不配置任何属性的默认效果:
可以看到大量的图形元素都一股脑的画上了,包括一个X轴,两个Y轴,网格线,图例等等,这些元素都是可以配置的。
数据层次结构
不同类型的图表用不同的 ChartView 来绘制,不同的 ChartView 需要的数据源 ChartData 也不一样,下面表格中列出 CharView 对应的 ChartData。
CharView 类型 | CharData 类型 |
---|---|
BarChartView | BarChartData |
BubbleChartView | BubbleChartData |
CandleStickChartView | CandleChartData |
PieChartView | PieChartData |
HorizontalBarChartView | BarChartData |
RadarChartView | RadarChartData |
ScatterChartView | ScatterChartData |
LineChartView | LineChartData |
CombinedChartView | 多种数据组合 |
每种类型的图都有一个 .data
属性,都对应一个 ChartData
,比较特殊的是 CombinedChartView
,它是组合图,会有 N 个不同的 data 属性。
再往下划分层次,可以看这个图:
-
ChartData
表示整个图的数据,一个ChartData
包含多个ChartDataSet
-
ChartDataSet
表示一张图上不同的“线”,一个ChartDataSet
包含多个ChartDataEntry
-
ChartDataEntry
表示一条“线”上不同的“点”。
对直方图 Bar Chart 来说,是这样一个结构:
BarChartView > BarChatData > BarChartDataSet > BarChartDataEntry
外观属性配置 - 日统计图
因为图表本身就很复杂,Charts 自定义的自由度很高,因此属性特别多,不过也有规律可循,先看一张图:
图中标注了大部分元素,有些在代码中直接对应单独的对象,包括
- X轴:chartView.xAxis,
Charts.XAxis
类型对象 - Y轴:chartView.leftAxis, chartView.rightAxis,
Charts.XAxis
类型对象 - 图例:chartView.legend,
Charts.Legend
类型对象
有些是包含在这些对象里面的:
- 网格线:分为X轴网格线(X Grid lines)和Y轴网格线(Y Grid lines),它们没有单独的对象,相关的属性都在X轴对象和Y轴对象里面。可以设置是否隐藏、虚线样式、粗细、颜色。
- 标签(label):显示在坐标轴上的刻度文字,默认直接显示刻度上X或Y的值,也可以用 Formatter 格式化成任意文字,同样它们需要在X轴对象和Y轴对象里面设置。
- 值(value):显示在图表中间区域的数据点Y值,也就是 ChartDataEntry 中的Y值,可以用 Formatter 格式化。
代码
chartView.legend.enabled = false // 图例说明,不显示
chartView.scaleXEnabled = false // X轴缩放功能,不开启
chartView.scaleYEnabled = false // Y轴缩放功能,不开启
chartView.doubleTapToZoomEnabled = false // 双击缩放功能,不开启
chartView.rightAxis.enabled = true // 右侧Y轴,显示
chartView.leftAxis.enabled = false // 左侧Y轴,不显示
let xAxis = chartView.xAxis // X轴
xAxis.axisMinimum = 0 // X轴最小值
xAxis.axisMaximum = 3 // X轴最大值
xAxis.axisLineWidth = 0 // X轴轴线宽度,0表示隐藏
xAxis.drawGridLinesEnabled = false // X轴格子线,与Y轴平行的直线集合,不显示
xAxis.drawLabelsEnabled = false // X轴标签,坐标轴上的数字或文字,不显示
let yAxis = chartView.rightAxis // 右侧Y轴
yAxis.axisMinimum = 0 // Y轴最小值
yAxis.axisMaximum = 12 // Y轴最大值
yAxis.drawGridLinesEnabled = false // Y轴格子线,与X轴平行的直线集合,不显示
yAxis.valueFormatter = TimeIntervalAxisShortFormatter() // Y轴标签格式化对象
yAxis.labelTextColor = UIColor.lightGray // Y轴标签字体颜色
if let font = UIFont(name: "HelveticaNeue", size: 10) { // 等宽字体,看起来整齐一点
yAxis.labelFont = font // Y轴标签字体
}
chartView.chartDescription?.enabled = false // 描述,可自定义文字内容,不显示
运行以上代码后,简洁多了:
这里有个坑
X轴或者Y轴有一个 labelCount
的整数属性,表示显示多少个 Label。但这个值设置了也有可能无效,它的注释是这样说的:
the number of label entries the axis should have.
max = 25, min = 2, default = 6
be aware that this number is not fixed and can only be approximated.
也就是说这个属性要在 [2, 25] 区间取值,其他值无效,而且即使在这个区间,也有可能无效。日统计图的 Y 轴取值范围是 [0, 12],单位是小时,这么设置没问题,不用设置 labelCount
就默认 7 个,正好。曾试过用秒做单位,取值区间就变为了 [0, 12*3600],结果 labelCount
就无法控制数量了,变成了固定 6 个,设置为 7 也不好使。后来发现还有个方法 func setLabelCount(_ count: Int, force: Bool)
可以强制设置数量,但也必须在 [2,25] 区间内。
外观属性配置 - 周统计图
设置上与日统计图大同小异,这里再放一遍图:
代码
chartView.noDataText = “啥也没有,别等了” // 未设置 .data 属性时显示的文字提示
chartView.legend.enabled = false
chartView.scaleXEnabled = false
chartView.scaleYEnabled = false
chartView.doubleTapToZoomEnabled = false
chartView.rightAxis.enabled = false // 隐藏右侧Y轴
let xAxis = chartView.xAxis // X轴
xAxis.labelPosition = .bottom // X轴显示在下方
xAxis.axisLineWidth = 1 // X轴轴线的宽度
xAxis.drawGridLinesEnabled = false // 不显示X轴网格线
// 这个 Formatter 可以直接将 [0, N] 的整数X值转换为字符串
xAxis.valueFormatter = IndexAxisValueFormatter(values:
["一","二","三","四","五","六","日"])
let yAxis = chartView.leftAxis
yAxis.axisMinimum = 0
yAxis.axisMaximum = 12
yAxis.valueFormatter = TimeIntervalAxisFormatter()
yAxis.gridLineDashLengths = [3, 3] // Y轴网格线显示为虚线
if let font = UIFont(name: "HelveticaNeue", size: 10) {
yAxis.labelFont = font
}
填充数据
周统计图比较复杂,下文仔细分析一下,日统计图代码比较简单附在后面不做详细介绍了。
周统计图是一个 Stacked Bar Chart,就是一个 Bar 其实是由多个 Bar 堆叠成的,可以用不同的颜色,也可以显示每个 Bar 的值。大多数图的 DataEntry 都是由一个X值和一个Y值组成的。但 BarChartDataEntry 还可以设置多个 Y 值,就能达到 Stacked Bar Chart 的效果。看内部源代码,这个所有Y值之和是有计算的,但是没有显示的设置,只能另辟蹊径来实现了。
但 Stacked Bar Chart 不会自动计算一个 BarChartDataEntry 中的所有Y值之和,也不会有显示。这时可以利用 Bar Chart 设置多个 DataSet 会重叠在一起的特性,再创建一个 DataSet 计算Y值之和,并设置颜色为透明,就可以显示出Y值之和了。
因此就有两个 Data Set 了:
- DataSet1:填充多个Y值的 Entry:
BarChartDataEntry(x: Double, yValues: [Double])
。显示堆叠不同颜色的条形图,隐藏 value 值,如果不隐藏每个不同颜色的块上都有一个 value。 - DataSet2:填充多个Y值之和的 Entry
BarChartDataEntry(x: Double, y: Double)
。只显示一个value值,需要将颜色设置为透明。
数据源分为两个数组:颜色数组,表示不同的种类;时间二维数组,第一维表示 X 值,第二维表示不同的颜色,正好能与颜色数组对应上。见下面代码:
func getBarData(colors: [String], values:[[TimeInterval]]) -> BarChartData {
// 第一个 DataSet 包含的 Entry 数组,多Y值 Entry
let yVal1 = (0..<values.count).map { (i) -> BarChartDataEntry in
return BarChartDataEntry(x: Double(i), yValues: values[i])
}
// 第二个 DataSet 包含的 Entry 数组,单Y值 Entry
let yVal2 = (0..<values.count).map { (i) -> BarChartDataEntry in
// 可以直接通过上一个多Y值 Entry 的 y 属性直接获得和
return BarChartDataEntry(x: Double(i), y: yVal1[i].y)
}
let barData = BarChartData()
if colors.count > 0 { // 注意颜色设置不能为空数组,这里简单判断了一下
// label 参数与图例有关,这里不显示图例,直接用空串了
let set = BarChartDataSet(values: yVal1, label: "")
set.colors = colors.map { UIColor(hex: $0)! } // 这里不能设置空数组,会崩溃
set.highlightEnabled = false // Bar 是否能被点击,点击后会有个默认的高亮效果
set.drawValuesEnabled = false // 不显示对应的 value
// 与左侧Y轴关联,.right 表示右侧Y轴。
// 两个Y轴可以有不同的取值范围,也可以关联不同的 DataSet。
// 一个Y轴可以对应多个 DataSet。
set.axisDependency = .left
barData.addDataSet(set)
let set2 = BarChartDataSet(values: yVal2, label: "")
set2.colors = [UIColor.clear]
set2.valueFormatter = TimeIntervalValueFormatter()
set2.highlightEnabled = false
set2.drawValuesEnabled = true
set2.axisDependency = .left
barData.addDataSet(set2)
}
// Bar 宽度,注意这里的单位是X值,而不是实际显示的尺寸。
barData.barWidth = 0.8
return barData
}
// 再设置上就可以了
chartView.data = getBarData(colors: colors, values: values)
下面是日统计图的数据设置代码:
func getChartData(colors: [String], values: [TimeInterval]) -> BarChartData {
let chartDataEntry = (0..<values.count).map {
BarChartDataEntry(x: Double($0), y: values[$0] / 3600.0)
}
// 这里动态设置了X轴取值范围,根据X值的数量来,为了美观,最小长度为3
let xMax = max(3, chartDataEntry.count)
chartView.xAxis.axisMinimum = 0 - Self.XOffset
chartView.xAxis.axisMaximum = Double(xMax) - Self.XOffset
let barData = BarChartData()
barData.barWidth = 0.8
if chartDataEntry.count > 0 {
let set = BarChartDataSet(values: chartDataEntry, label: "")
set.valueFormatter = self
set.colors = colors.map { UIColor(hex: $0)! }
set.highlightEnabled = false
set.drawValuesEnabled = true
set.axisDependency = .right // 关联右侧Y轴
barData.addDataSet(set)
}
return barData
}
总结以及一些有用的结论
- 图表在 View 中的绘制是 ScaleToFill 模式,即缩放拉伸图表适应整个 View 的大小。
- 所有的 label、value 值都是 Double 类型的,可以通过 Formatter 对象来格式化。
- DataEntry 可以关联一个任意对象,可以用在格式化、Marker显示、点击事件里,弥补 DataEntry 中的 XY值不能包含的数据。
- XY坐标轴的范围可以不设置,这时会通过数据自动推断,效果是尽可能填满整个 View。
- X轴默认是在上面的,可以设置为下面
xAxis.labelPosition = .bottom
- BarChartView 中的 Bar 是有宽度的,是按数据大小来设置(即X值),而不是按显示的大小。使用
BarChartData.barWidth
设置。 - BarChartData 设置多个 DataSet 会重叠在一起,需要调用
BarChartData.groupBars()
方法来设置才能分开。 - BarChartDataEntry 可以设置多个Y值,这样画出来的就是 Stacked Bar Chart。