细品Swift - 一目了然的图表Swift Charts(2)
Bar Chart 条形图
Bar Chart的维基百科解释如下:
条形图(英语:bar chart),或 条图(英语:bar graph),台湾常称为 长条图,又称为 柱状图、棒形图,是一种以长方形的长度为变量的统计图表。长条图用来比较两个或以上的价值(不同时间或者不同条件),只有一个变量,通常利用于较小的数据集分析。长条图亦可横向排列,或用多维方式表达。绘制长条图时,长条柱或柱组中线须对齐项目刻度。相较之下,折线图则是将数据代表之点对齐项目刻度。在数字大且接近时,两者皆可使用波浪形省略符号,以扩大表现数据间的差距,增强理解和清晰度。
长条图通常适用于较小的数据集分析,其每个数据标记通常表示一个分类的数量或者几个分类的数量总和。
基本BarChart
在一个图表中可以对同一数据使用不同的标记来描绘,如BarMark是条状图样式的标记,PointMark是点状样式的标记,LineMark是折线样式的标记。
BarChart可以从不同的维度来对数据进行可视化,比如本图表按照食物的名字或者食物的种类,分类来统计对应的食物总重量。
BarChart会将x值相同的数据堆叠在一个条形图上,通过修饰语句.foregroundStyle可以把同一条形上的不同数据按照指定的value分开。 这里指定的value会被当作图例使用。同样,可以指定图例的颜色,这里是通过对Chart的修饰语句.chartForegroundStyleScale来指定图例对应的颜色。
var basicBarChart: some View {
VStack {
VStack {
Divider()
Text("用$0直接表示单个数据").font(.footnote)
// Chart使用类似于List的方式,从foods中直接拆解处单个数据并绘制
Chart(foods) {
BarMark(x: .value("name", $0.name), y: .value("weight", $0.weight))
}
}
VStack {
Divider()
Text("数据标记的选择决定了图表的类别").font(.footnote)
// 数据标记(mark)可以选择多种表示方式(点PointMark、条形BarMark、折线LineMark、矩形RectangleMark等)
// 本图表对每个数据选择了两种标记方式: BarMark和LineMark
Chart(foods) { food in
BarMark(x: .value("name", food.name), y: .value("weight", food.weight))
LineMark(x: .value("name", food.name), y: .value("weight", food.weight))
.foregroundStyle(.red)
}
}
VStack {
Divider()
Text("从不同维度对数据进行观察的BarChart").font(.footnote)
// 可以选择不同的数据属性作为X轴,以从不同的维度对数据进行观察
// 如多个数据具有相同的x轴维度值,Chart会把他们直接合并,显示所有对应的y轴值之和。
// 本例中把所有的数据按照type维度,分成4个条状图。
Chart(foods) { food in
BarMark(x: .value("type", food.type) ,
y: .value("weight", food.weight))
}
}
VStack {
Divider()
Text("合并后在每个Bar中显示原始数据所占部分").font(.footnote)
// 使用修饰指令.foregroundStyle(by: PlottableValue)
// 可以看出,图表中多了legend(图例)部分,指定每种颜色对应的数据,但可以看出
Chart(foods) { food in
BarMark(x: .value("type", food.type) ,
y: .value("weight", food.weight))
// 在每个Bar上为每个color属性的值赋不同的颜色
// 同一bar上同颜色的数据再次合并, 如Avocado和GreenGrape的食物类型相同,所有都在Fruit Bar上,进一步,他们的颜色都是Green,所以以同样的颜色显示在Bar上。
.foregroundStyle(by: .value("color", food.color))
}
}
VStack {
Divider()
Text("指定显示每种图例对应的颜色").font(.footnote)
// 用chartForgroundStyleScale来指定每种图例对应的颜色
Chart(foods) { food in
BarMark(x: .value("type", food.type) ,
y: .value("weight", food.weight))
// 在每个Bar上为每个color属性的值赋不同的颜色
// 同一bar上同颜色的数据再次合并, 如Avocado和GreenGrape的食物类型相同,所有都在Fruit Bar上,进一步,他们的颜色都是Green,所以以同样的颜色显示在Bar上。
.foregroundStyle(by: .value("color", food.color.description))
}
.chartForegroundStyleScale([
"yellow-color": .yellow,
"green-color": .green,
"red-color": .red,
"white-color": .gray.opacity(0.5), // 用半透明的gray来标识白色
"pink-color": .pink,
"gray-color": .gray
])
}
VStack {
Divider()
Text("从颜色维度来分类并显示每条Bar中食物种类分布").font(.footnote)
Chart(foods) { food in
BarMark(x: .value("color", food.color) ,
y: .value("weight", food.weight))
// 在每个Bar上为每个color属性的值赋不同的颜色
// 同一bar上同颜色的数据再次合并, 如Avocado和GreenGrape的食物类型相同,所有都在Fruit Bar上,进一步,他们的颜色都是Green,所以以同样的颜色显示在Bar上。
.foregroundStyle(by: .value("type", food.type))
}
// 按照食物种类指定颜色
.chartForegroundStyleScale([
"Meat": .pink,
"Vegetable": .green,
"Grain": .brown,
"Fruit": .orange
])
}
}
}

image.png
一维BarChart和区间BarChart
BarChart可以用于展示一维数据图以及区间图。
如果需要在一维上展示数据,只需要输入x或y中的一个数据,以及图例基于的数据。
区间图需要提供在一个维度上的数据的上下区间值,已经另外一个维度的数据属性。
var advancedBarChart: some View {
VStack {
VStack {
Divider()
Text("一维条形图").font(.footnote)
// 按照种类排序后,同种类的食物连续显示
Chart(foods.sorted(by: {$0.type < $1.type})) {
BarMark(x: .value("weight", $0.weight))
.foregroundStyle(by: .value("type", $0.type))
}
}
VStack {
Divider()
Text("区间条形图").font(.footnote)
// 用条状图来标识一个区间,特别是时间区间,如甘特图。
Chart(foods) { food in
BarMark(x: .value("name", food.name),
yStart: .value("minGi", food.minGiValue),
yEnd: .value("maxGi", food.maxGiValue)
)
}
}
VStack {
Divider()
Text("横向区间条形图").font(.footnote)
Chart(foods) { food in
BarMark(xStart: .value("minGi", food.minGiValue), xEnd: .value("maxGi", food.maxGiValue),
y: .value("color", food.color))
.foregroundStyle(by: .value("type", food.type))
}
}
}
}

image.png
完整演示代码
完整的演示代码如下:
struct Food: Identifiable {
let id = UUID()
let name: String
let maxGiValue: Int
let minGiValue: Int
let maxLength: Double
let minLength: Double
let color: String
let type: String
let weight: Double
}
let foods: [Food] = [
Food(name: "Cabbage", maxGiValue: 13, minGiValue: 8, maxLength: 23, minLength: 10, color: "green-color", type: "Vegetable", weight: 2),
Food(name: "Beef", maxGiValue: 54, minGiValue: 18, maxLength: 53, minLength: 20, color: "pink-color", type: "Meat", weight: 1.8),
Food(name: "Rice", maxGiValue: 93, minGiValue: 78, maxLength: 5, minLength: 2, color: "gray-color", type: "Grain", weight: 8),
Food(name: "Tomato", maxGiValue: 33, minGiValue: 21, maxLength: 13, minLength: 5, color: "red-color", type: "Vegetable", weight: 5),
Food(name: "Pork", maxGiValue: 63, minGiValue: 38, maxLength: 63, minLength: 10, color: "pink-color", type: "Meat", weight: 5.3),
Food(name: "Fish", maxGiValue: 49, minGiValue: 28, maxLength: 53, minLength: 5, color: "white-color", type: "Meat", weight: 1.5),
Food(name: "Flour", maxGiValue: 83, minGiValue: 3, maxLength: 0.1, minLength: 10, color: "white-color", type: "Grain", weight: 6.5),
Food(name: "Banana", maxGiValue: 93, minGiValue: 78, maxLength: 33, minLength: 10, color: "yellow-color", type: "Fruit", weight: 3.8),
Food(name: "Avocado", maxGiValue: 33, minGiValue: 18, maxLength: 25, minLength: 8, color: "green-color", type: "Fruit", weight: 8),
Food(name: "GreenGrape", maxGiValue: 99, minGiValue: 68, maxLength: 23, minLength: 10, color: "green-color", type: "Fruit", weight: 8)
]
struct AllAboutBarChartTest: View {
var body: some View {
basicBarChart
advancedBarChart
}
var basicBarChart: some View {
VStack {
VStack {
Divider()
Text("用$0直接表示单个数据").font(.footnote)
// Chart使用类似于List的方式,从foods中直接拆解处单个数据并绘制
Chart(foods) {
BarMark(x: .value("name", $0.name), y: .value("weight", $0.weight))
}
}
VStack {
Divider()
Text("数据标记的选择决定了图表的类别").font(.footnote)
// 数据标记(mark)可以选择多种表示方式(点PointMark、条形BarMark、折线LineMark、矩形RectangleMark等)
// 本图表对每个数据选择了两种标记方式: BarMark和LineMark
Chart(foods) { food in
BarMark(x: .value("name", food.name), y: .value("weight", food.weight))
LineMark(x: .value("name", food.name), y: .value("weight", food.weight))
.foregroundStyle(.red)
}
}
VStack {
Divider()
Text("从不同维度对数据进行观察的BarChart").font(.footnote)
// 可以选择不同的数据属性作为X轴,以从不同的维度对数据进行观察
// 如多个数据具有相同的x轴维度值,Chart会把他们直接合并,显示所有对应的y轴值之和。
// 本例中把所有的数据按照type维度,分成4个条状图。
Chart(foods) { food in
BarMark(x: .value("type", food.type) ,
y: .value("weight", food.weight))
}
}
VStack {
Divider()
Text("合并后在每个Bar中显示原始数据所占部分").font(.footnote)
// 使用修饰指令.foregroundStyle(by: PlottableValue)
// 可以看出,图表中多了legend(图例)部分,指定每种颜色对应的数据,但可以看出
Chart(foods) { food in
BarMark(x: .value("type", food.type) ,
y: .value("weight", food.weight))
// 在每个Bar上为每个color属性的值赋不同的颜色
// 同一bar上同颜色的数据再次合并, 如Avocado和GreenGrape的食物类型相同,所有都在Fruit Bar上,进一步,他们的颜色都是Green,所以以同样的颜色显示在Bar上。
.foregroundStyle(by: .value("color", food.color))
}
}
VStack {
Divider()
Text("指定显示每种图例对应的颜色").font(.footnote)
// 用chartForgroundStyleScale来指定每种图例对应的颜色
Chart(foods) { food in
BarMark(x: .value("type", food.type) ,
y: .value("weight", food.weight))
// 在每个Bar上为每个color属性的值赋不同的颜色
// 同一bar上同颜色的数据再次合并, 如Avocado和GreenGrape的食物类型相同,所有都在Fruit Bar上,进一步,他们的颜色都是Green,所以以同样的颜色显示在Bar上。
.foregroundStyle(by: .value("color", food.color.description))
}
.chartForegroundStyleScale([
"yellow-color": .yellow,
"green-color": .green,
"red-color": .red,
"white-color": .gray.opacity(0.5), // 用半透明的gray来标识白色
"pink-color": .pink,
"gray-color": .gray
])
}
VStack {
Divider()
Text("从颜色维度来分类并显示每条Bar中食物种类分布").font(.footnote)
Chart(foods) { food in
BarMark(x: .value("color", food.color) ,
y: .value("weight", food.weight))
// 在每个Bar上为每个color属性的值赋不同的颜色
// 同一bar上同颜色的数据再次合并, 如Avocado和GreenGrape的食物类型相同,所有都在Fruit Bar上,进一步,他们的颜色都是Green,所以以同样的颜色显示在Bar上。
.foregroundStyle(by: .value("type", food.type))
}
// 按照食物种类指定颜色
.chartForegroundStyleScale([
"Meat": .pink,
"Vegetable": .green,
"Grain": .brown,
"Fruit": .orange
])
}
}
}
var advancedBarChart: some View {
VStack {
VStack {
Divider()
Text("一维条形图").font(.footnote)
// 按照种类排序后,同种类的食物连续显示
Chart(foods.sorted(by: {$0.type < $1.type})) {
BarMark(x: .value("weight", $0.weight))
.foregroundStyle(by: .value("type", $0.type))
}
}
VStack {
Divider()
Text("区间条形图").font(.footnote)
// 用条状图来标识一个区间,特别是时间区间,如甘特图。
Chart(foods) { food in
BarMark(x: .value("name", food.name),
yStart: .value("minGi", food.minGiValue),
yEnd: .value("maxGi", food.maxGiValue)
)
}
}
VStack {
Divider()
Text("横向区间条形图").font(.footnote)
Chart(foods) { food in
BarMark(xStart: .value("minGi", food.minGiValue), xEnd: .value("maxGi", food.maxGiValue),
y: .value("color", food.color))
.foregroundStyle(by: .value("type", food.type))
}
}
}
}
}
总结如下:
- BarChart中文名称为条状图,柱状图等,是图形化少量数据的有效方法。
- BarChart可以在多种维度上展示数据。
- BarChart可以用图例更详细的演示堆叠在一个Bar上的多种数据。
- BarChart可以以一维的方式展示数据。
- BarChart可以展示数据区间。