美文网首页
iOS - Charts 直方图

iOS - Charts 直方图

作者: 拾识物者 | 来源:发表于2019-03-18 22:47 被阅读0次

关键字:iOS Charts 图表 swift BarChart 直方图

本文给出两个使用 danielgindi/Charts > GitHub > 的示例,以及在使用过程中遇到的坑。

本人正在开发一个有关时间统计的小应用,其中有两个统计图的功能:一个是日统计图,另一个是周统计图。

最终效果

日统计图:X轴颜色,Y轴时间

日统计图是一个直方图,因为用在列表中,尺寸不能太大,所以比较简单,没有网格线,只有右侧的Y轴刻度。X轴表示不同的颜色,Y轴表示时间,显示为 “HH:mm” 格式。

周统计图:X轴星期,Y轴时间

周统计图也是一个直方图,用在单独界面中的,尺寸比较大,可以显示更复杂的数据。这种一个X坐标上堆叠好几个条块的直方图叫 Stacked Bar Chart,也就是一个X值对应多个Y值并画成堆叠的形式。

Charts 使用

安装

用 CocoaPods 安装

pod 'Charts'

使用

如果使用 storyboard 或者 xib,先拖拽一个 UIView,并且设置 Custom Class 为 BarChartViewBarChartView 表示直方图,如果是其他类型的图,需要使用对应的 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] 区间内。

外观属性配置 - 周统计图

设置上与日统计图大同小异,这里再放一遍图:

周统计图:X轴星期,Y轴时间

代码

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 了:

  1. DataSet1:填充多个Y值的 Entry: BarChartDataEntry(x: Double, yValues: [Double])。显示堆叠不同颜色的条形图,隐藏 value 值,如果不隐藏每个不同颜色的块上都有一个 value。
  2. 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。

相关文章

网友评论

      本文标题:iOS - Charts 直方图

      本文链接:https://www.haomeiwen.com/subject/iyxymqtx.html