使用 Swift Charts 框架创建饼图或圆环图
了解如何使用 Swift Charts 框架,为应用添加美观具有交互性的饼图或圆环图。
Swift Charts 是 Apple 在 iOS 16 上推出的一个图表绘制框架,它提供了一套直观且强大的 API,帮助开发者快速构建各种类型的图表。相比于第三方开源库(如 Charts
框架),Swift Charts 是系统原生的,拥有更高的性能优化,同时与 SwiftUI 紧密集成,使图表的添加更加方便和高效。
Swift Charts 提供了多种常见的图表类型,包括:
- 条形图(Bar Chart)
- 折线图(Line Chart)
- 饼图(Sector Chart)
- 散点图(Point Chart)
- 等等
要开始使用 Swift Charts,首先确保你的开发环境是 iOS 16 或更高版本,并且项目支持 SwiftUI。
Swift Chart 基础用法
如果你还不了解 Swift Charts 的基本用法,或希望使用它创建条形图、折线图或散点图,建议你先阅读我之前的这篇教程:
创建饼图(SectorMark)
WWDC23 的这个 Session 对饼图做了比较详细的介绍,你可以作为补充阅读:
SectorMark 的基本语法如下:
SectorMark(
angle: .value("角度", 数据),
innerRadius: .value("内半径", 半径值),
outerRadius: .value("外半径", 半径值)
)
- angle: 这是扇形的角度值,通常根据数据的大小来决定。它定义了每个扇区在圆环中占据的比例。
- innerRadius: 定义扇形的内半径,决定了图表是否为饼图还是圆环图。如果内半径为0,则是饼图;否则就是圆环图。
- outerRadius: 定义外半径,决定了扇形的外边界。
这些参数都是 PlottableValue
类型,可以通过 .value()
或 .plottedValue()
传入具体的值或数据映射。
下面的例子展示了如何使用 SectorMark
来生成一个展示不同水果销量占比的圆环图。
//
// FruitSalesData.swift
//
// Created by Ivens Liao on 2024/10/14.
//
import Charts
import SwiftUI
// 数据模型
struct FruitSalesData: Identifiable {
let id = UUID()
let fruit: String
let sales: Double
}
// 示例数据
let fruitSales: [FruitSalesData] = [
FruitSalesData(fruit: "苹果", sales: 35),
FruitSalesData(fruit: "香蕉", sales: 25),
FruitSalesData(fruit: "橙子", sales: 20),
FruitSalesData(fruit: "葡萄", sales: 10),
FruitSalesData(fruit: "草莓", sales: 10),
]
// Sector Chart 视图
struct SectorChartView: View {
var body: some View {
Chart(fruitSales) { item in
SectorMark(
angle: .value("销售额", item.sales),
outerRadius: .ratio(1.0) // 外半径设置为全图半径
)
.foregroundStyle(by: .value("水果", item.fruit))
}
.frame(height: 300)
.chartLegend(position: .bottom) // 将图例显示在图表下方
.padding()
}
}
// Preview,提供实时预览
struct SectorChartView_Previews: PreviewProvider {
static var previews: some View {
SectorChartView()
}
}
我们通过 SectorMark
的 angle
属性将每种水果的销售数据映射为扇形的角度,而 outerRadius
设置为 1.0
,表示占据整个图的外半径。
上面这个示例比较简单。在实际中,我们经常需要根据基础数据,合并计算某些类别的总和值,然后统计类别的占比。
示例代码:
//
// FoodSalesData.swift
// MONO
//
// Created by Ivens Liao on 2024/10/14.
//
import Charts
import SwiftUI
// 数据模型
struct FoodSalesData: Identifiable {
let id = UUID()
let name: String
let category: String
let sales: Double
}
// 示例数据
let individualFoodSales: [FoodSalesData] = [
FoodSalesData(name: "香蕉", category: "水果", sales: 25),
FoodSalesData(name: "苹果", category: "水果", sales: 35),
FoodSalesData(name: "橙子", category: "水果", sales: 20),
FoodSalesData(name: "鸡肉", category: "肉类", sales: 40),
FoodSalesData(name: "猪肉", category: "肉类", sales: 30),
FoodSalesData(name: "牛肉", category: "肉类", sales: 20),
FoodSalesData(name: "可乐", category: "饮料", sales: 50),
FoodSalesData(name: "薯片", category: "零食", sales: 30),
]
// 按类别汇总销售额
func groupSalesByCategory(_ salesData: [FoodSalesData]) -> [FoodSalesData] {
var categorySales: [String: Double] = [:]
// 汇总每个类别的销售额
for item in salesData {
categorySales[item.category, default: 0] += item.sales
}
// 将汇总结果转换为 FoodSalesData 数组
return categorySales.map { FoodSalesData(name: $0.key, category: $0.key, sales: $0.value) }
}
// Sector Chart 视图
struct SectorChartView: View {
let groupedSales = groupSalesByCategory(individualFoodSales) // 分类后的销售数据
var body: some View {
Chart(groupedSales) { item in
SectorMark(
angle: .value("销售额", item.sales),
outerRadius: .ratio(1.0)
)
.foregroundStyle(by: .value("类别", item.category))
}
//.frame(height: 300)
.chartLegend(position: .bottom)
.padding()
}
}
// Preview,提供实时预览
struct SectorChartView_Previews: PreviewProvider {
static var previews: some View {
SectorChartView()
}
}
构建圆环图(innerRadius)
只需调整 innerRadius
的指,即可得到圆环图效果:
Chart(groupedSales) { item in
SectorMark(
angle: .value("销售额", item.sales),
innerRadius: .ratio(0.5),
outerRadius: .ratio(1.0)
)
.foregroundStyle(by: .value("类别", item.category))
}
.chartLegend(position: .bottom)
.padding()
innerRadius: .ratio(0.618)
, 黄金比例。自定义属性
为每个区间添加标签(annotation)
使用 annotation
为每个扇区中显示对应的标签值,方便用户理解具体的比例。
Chart(groupedSales) { item in
SectorMark(
angle: .value("销售额", item.sales),
innerRadius: .ratio(0.5),
outerRadius: .ratio(1.0)
)
.foregroundStyle(by: .value("类别", item.category))
.annotation(position: .overlay) {
VStack {
Text("\(item.category)")
Text("\(item.sales, specifier: "%.1f")%")
}
.font(.caption)
.foregroundColor(.white)
}
}
.chartLegend(position: .bottom)
.padding()
设置扇形的角度内缩量(angularInset)
通过指定 angularInset
,你可以在每个扇形之间增加一个内缩的角度,使扇形之间产生分隔效果,从而避免各个部分直接相连。
Chart(groupedSales) { item in
SectorMark(
angle: .value("销售额", item.sales),
innerRadius: .ratio(0.5),
outerRadius: .ratio(1.0),
angularInset: 2 // 设置 2 度的角度内缩量
)
.foregroundStyle(by: .value("类别", item.category))
.annotation(position: .overlay) {
VStack {
Text("\(item.category)")
Text("\(item.sales, specifier: "%.1f")%")
}
.font(.caption)
.foregroundColor(.white)
}
}
.chartLegend(position: .automatic)
.padding()
如果你将 angularInset
设置为 0
,所有的扇形部分会直接相连。
创建个性化外观效果
组合使用 angularInset
和 .cornerRadius(8.0)
,可以得到如下效果:
Chart(groupedSales) { item in
SectorMark(
angle: .value("销售额", item.sales),
innerRadius: .ratio(0.5),
outerRadius: .ratio(1.0),
angularInset: 5.0 // 设置 2 度的角度内缩量
)
.cornerRadius(6.0)
.foregroundStyle(by: .value("类别", item.category))
.annotation(position: .overlay) {
VStack {
Text("\(item.category)")
Text("\(item.sales, specifier: "%.1f")%")
}
.font(.caption)
.foregroundColor(.white)
}
}
.chartLegend(position: .automatic)
.padding()
或者创建扇形图效果:
突出显示某个扇形区域
通过条件判断设置 outerRadius 参数的值,可以实现类似这样的效果:
Chart(groupedSales) { item in
SectorMark(
angle: .value("销售额", item.sales),
outerRadius: item.category
== "水果" ? 160 : 150,
angularInset: 4.0 // 设置 2 度的角度内缩量
)
.cornerRadius(8.0)
.foregroundStyle(by: .value("类别", item.category))
.annotation(position: .overlay) {
VStack {
Text("\(item.category)")
Text("\(item.sales, specifier: "%.1f")%")
}
.font(.caption)
.foregroundColor(.white)
}
}
.chartLegend(position: .automatic)
.padding()
这种方式更加使用扇形图而不是圆环图。
添加背景标题说明(chartBackground)
Chart(groupedSales) { item in
SectorMark(
angle: .value("销售额", item.sales),
innerRadius: .ratio(0.618), // 可选,设置内半径,形成环形图
outerRadius: .ratio(1.0),
angularInset: 2.0 // 设置 2 度的角度内缩量
)
.cornerRadius(8.0)
.foregroundStyle(by: .value("类别", item.category))
.annotation(position: .overlay) {
VStack {
Text("\(item.category)")
Text("\(item.sales, specifier: "%.1f")")
}
.font(.caption)
.foregroundColor(.white)
}
}
.chartBackground { proxy in
VStack {
Text("最佳销售类别")
.font(.callout)
.foregroundStyle(Color.secondary)
Text("肉类")
.font(.title2.bold())
.foregroundStyle(Color.primary)
}
}
.chartLegend(position: .automatic)
.padding()
为扇形区域添加点击交互(chartAngleSelection)
[TODO] - 如果我忘记更新,记得评论提醒我。
欢迎你订阅我的博客 Code with Ivens,获取更多关于 Swift 和 iOS 开发的实用教程与技巧。