一、简单折线图
苹果在 WWWDC 2022 上推出了 SwiftUI 图表,这使得在 SwiftUI 视图中创建图表变得异常简单。图表是以丰富的格式呈现可视化数据的一种很好的方式,而且易于理解。本文展示了如何用比以前从头开始创建同样的折线图少得多的代码轻松创建折线图,此外自定义图表的外观和感觉以及使图表中的信息易于访问也是非常容易的。 从包含王者荣耀全部英雄的登场率开始,类似于在 SwiftUI 中创建折线图中使用的数据,定义一个结构来保存英雄和登场率的步数,并创建一个数组:
struct KingHeroWinRate : Identifiable {
let id = UUID ( )
let hero: String
let winRate: Double
init ( hero: String , winRate: Double ) {
self . hero = hero;
self . winRate = winRate;
}
}
let currentHero: [ KingHeroWinRate ] = [
KingHeroWinRate ( hero: "孙悟空" , winRate: 0.140 ) ,
KingHeroWinRate ( hero: "玄奘" , winRate: 0.186 ) ,
KingHeroWinRate ( hero: "猪八戒" , winRate: 0.111 ) ,
KingHeroWinRate ( hero: "嫦娥" , winRate: 0.187 ) ,
KingHeroWinRate ( hero: "杨戬" , winRate: 0.084 )
]
要创建一个折线图,为英雄登场率数据中的每个元素创建一个带有 LineMark 的图表,在 LineMark 的 X 值中指定英雄,在 Y 值中指定登场率,需要注意的是需要导入 Charts 框架。这就为英雄登场率数据创建了一个线形图,由于只有一个系列的数据,ForEach 可以省略,数据可以直接传递给 Chart 初始化器,两个部分都产生相同的折线图:
import SwiftUI
import Charts
struct ContentView : View {
var body: some View {
VStack {
GroupBox ( "王者荣耀英雄登场率排行" ) {
Chart {
ForEach ( currentHero) {
LineMark (
x: . value ( "英雄" , $0 . hero) ,
y: . value ( "胜率" , $0 . winRate)
)
}
}
}
GroupBox ( "王者荣耀英雄登场率排行" ) {
Chart ( currentHero) {
LineMark (
x: . value ( "英雄" , $0 . hero) ,
y: . value ( "胜率" , $0 . winRate)
)
}
}
}
}
}
使用 SwiftUI Charts 创建折线图显示英雄登场率的效果如下:
二、其它图表
SwiftUI Charts 有许多可用的图表选项,这些可以通过将图表标记从 LineMark 改为其他类型的标记(如 BarMark)来生成条形图:
struct ContentView : View {
var body: some View {
VStack {
GroupBox ( "王者荣耀英雄登场率排行" ) {
Chart {
ForEach ( currentHero) {
LineMark (
x: . value ( "英雄" , $0 . hero) ,
y: . value ( "胜率" , $0 . winRate)
)
}
}
}
GroupBox ( "王者荣耀英雄登场率排行" ) {
Chart ( currentHero) {
BarMark (
x: . value ( "英雄" , $0 . hero) ,
y: . value ( "胜率" , $0 . winRate)
)
}
}
GroupBox ( "王者荣耀英雄登场率排行" ) {
Chart ( currentHero) {
PointMark (
x: . value ( "英雄" , $0 . hero) ,
y: . value ( "胜率" , $0 . winRate)
)
}
}
GroupBox ( "王者荣耀英雄登场率排行" ) {
Chart ( currentHero) {
RectangleMark (
x: . value ( "英雄" , $0 . hero) ,
y: . value ( "胜率" , $0 . winRate)
)
}
}
GroupBox ( "王者荣耀英雄登场率排行" ) {
Chart ( currentHero) {
AreaMark (
x: . value ( "英雄" , $0 . hero) ,
y: . value ( "胜率" , $0 . winRate)
)
}
}
}
}
}
三、增加折线图的可访问性
将图表植入 SwiftUI 的一个好处是,可以很容易地使用可访问性修饰符使图表变得可访问。为 KingHeroWinRate 添加一个计算属性,将数据返回为一个字符串,可由 accessibilityLabel 使用,然后为图表中的每个标记添加可访问性标签和值:
struct KingHeroWinRate : Identifiable {
let id = UUID ( )
let hero: String
let winRate: Double
init ( hero: String , winRate: Double ) {
self . hero = hero;
self . winRate = winRate;
}
var heroNameString: String {
return hero;
}
}
struct ContentView : View {
var body: some View {
VStack {
GroupBox ( "王者荣耀英雄登场率排行" ) {
Chart {
ForEach ( heroData, id: \ . period) {
ForEach ( $0 . data) {
LineMark (
x: . value ( "英雄" , $0 . hero) ,
y: . value ( "登场率" , $0 . winRate)
)
. accessibilityLabel ( $0 . heroNameString)
. accessibilityValue ( " \( $0 . winRate) winRate" )
}
}
}
}
}
}
}
四、为折线图添加多个数据序列
折线图是比较两个不同系列数据的好方法,创建第二个系列,即另外一组英雄的登场率,并将这两个系列添加到折线图中:
let previousHero: [ KingHeroWinRate ] = [
KingHeroWinRate ( hero: "刘备" , winRate: 0.063 ) ,
KingHeroWinRate ( hero: "关羽" , winRate: 0.071 ) ,
KingHeroWinRate ( hero: "赵云" , winRate: 0.079 ) ,
KingHeroWinRate ( hero: "张飞" , winRate: 0.061 ) ,
KingHeroWinRate ( hero: "黄忠" , winRate: 0.061 )
]
let currentHero: [ KingHeroWinRate ] = [
KingHeroWinRate ( hero: "孙悟空" , winRate: 0.140 ) ,
KingHeroWinRate ( hero: "玄奘" , winRate: 0.186 ) ,
KingHeroWinRate ( hero: "猪八戒" , winRate: 0.111 ) ,
KingHeroWinRate ( hero: "嫦娥" , winRate: 0.187 ) ,
KingHeroWinRate ( hero: "杨戬" , winRate: 0.084 )
]
let heroData = [
( period: "Current Hero" , data: currentHero) ,
( period: "Previous Hero" , data: previousHero)
]
GroupBox ( "王者荣耀英雄登场率排行" ) {
Chart {
ForEach ( heroData, id: \ . period) {
ForEach ( $0 . data) {
LineMark (
x: . value ( "英雄" , $0 . heroBranching) ,
y: . value ( "登场率" , $0 . winRate)
)
. accessibilityLabel ( $0 . heroBranching)
. accessibilityValue ( " \( $0 . winRate) winRate" )
}
}
}
}
五、显示登场率系列
在折线图中显示多个基于英雄的登场率系列,最初尝试在折线图中显示多组数据的问题是 X 轴使用了英雄,当前的英雄紧接着上一部分英雄,因此每一个点都是沿着 X 轴线性递增绘制的。有必要只用分路作为 X 轴的数值,这样所有的英雄都在同一个 X 坐标上绘制。 在 KingHeroWinRate 中添加另一个计算属性,以便以字符串格式返回英雄的分路:
struct KingHeroWinRate : Identifiable {
let id = UUID ( )
let hero: String
let winRate: Double
init ( hero: String , winRate: Double ) {
self . hero = hero;
self . winRate = winRate;
}
var heroNameString: String {
return hero;
}
var heroBranching: String {
if ( heroNameString == "孙悟空" || heroNameString == "刘备" ) {
return "打野" ;
} else if ( heroNameString == "玄奘" || heroNameString == "关羽" ) {
return "中路" ;
} else if ( heroNameString == "猪八戒" || heroNameString == "赵云" ) {
return "上路" ;
} else if ( heroNameString == "嫦娥" || heroNameString == "张飞" ) {
return "辅助" ;
} else if ( heroNameString == "杨戬" || heroNameString == "黄忠" ) {
return "下路" ;
} else {
return "" ;
}
}
}
此 heroBranching 用于图表中 LineMarks 的 x 值。另外,前景的样式设置为基于 KingHeroWinRate 数组的周期,折线图使用 x 轴的分路来显示英雄,以便在分路之间进行比较:
struct ContentView : View {
var body: some View {
VStack {
GroupBox ( "王者荣耀英雄登场率排行" ) {
Chart {
ForEach ( heroData, id: \ . period) { hero in
ForEach ( hero. data) {
LineMark (
x: . value ( "英雄" , $0 . heroBranching) ,
y: . value ( "登场率" , $0 . winRate)
)
. foregroundStyle ( by: . value ( "branch" , hero. period) )
. accessibilityLabel ( $0 . heroBranching)
. accessibilityValue ( " \( $0 . winRate) winRate" )
}
}
}
. frame ( height: 400 )
}
. padding ( )
Spacer ( )
}
}
}