iOS - Charts 直方图

关键字: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。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,386评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,142评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,704评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,702评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,716评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,573评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,314评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,230评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,680评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,873评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,991评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,706评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,329评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,910评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,038评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,158评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,941评论 2 355