一、图表布局
条形(柱状)图以矩形条的形式呈现数据的类别,其宽度和高度与它们表示的值成比例。SwiftUI 对探索不同布局和预览实时视图结果是很友好的,很容易将部分内容提取到子视图中,以便每个部分都很小且易于维护。 从包含 HistogramView 以及可能的其它文本或数据的视图开始,HistogramView 包含一个标题和一个图表区,它们由文本和圆角矩形表示。 HistogramView 的创建:
struct HistogramView : View {
var title: String
var body: some View {
GeometryReader { gr in
let headHeight = gr. size. height * 0.10
VStack {
HistogramHeaderView ( title: title, height: headHeight)
HistogramAreaView ( )
}
}
}
}
struct HistogramHeaderView : View {
var title: String
var height: CGFloat
var body: some View {
Text ( title)
. frame ( height: height)
}
}
struct HistogramAreaView : View {
var body: some View {
ZStack {
RoundedRectangle ( cornerRadius: 5.0 )
. fill ( Color ( #colorLiteral ( red: 0.80 , green: 0.90 , blue: 0.80 , alpha: 1 ) ) )
}
}
}
在 ContentView 中调用 HistogramView:
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView ( )
}
}
struct ContentView : View {
var body: some View {
VStack {
Text ( "数据分析" )
. font ( . title)
HistogramView (
title: "柱状图" )
. frame ( width: 300 , height: 300 , alignment: . center)
Spacer ( )
}
. padding ( )
}
}
二、添加条形(柱状)图
定义一些简单的数据类别,如一周内每天看手机的时间分钟数,以下表数据被作为主视图的项目数据,每一条数据包含一个键值对(名称和值)。在平常的 App 里,这些数据应该通过 ViewModel 从 model 里取数据:
Day Times 星期一 188 星期二 209 星期三 300 星期四 150 星期五 198 星期六 488 星期日 409
struct ContentView : View {
let chartData: [ DataItem ] = [
DataItem ( name: "星期一" , value: 188 ) ,
DataItem ( name: "星期二" , value: 209 ) ,
DataItem ( name: "星期三" , value: 300 ) ,
DataItem ( name: "星期四" , value: 150 ) ,
DataItem ( name: "星期五" , value: 198 ) ,
DataItem ( name: "星期六" , value: 488 ) ,
DataItem ( name: "星期日" , value: 409 )
]
var body: some View {
VStack {
Text ( "屏幕使用时间" )
. font ( . title)
HistogramView (
title: "一周数据分析" , data: chartData)
. frame ( width: 350 , height: 500 , alignment: . center)
Spacer ( )
}
}
}
更新 HistogramView 使数据可以作为参数传递到 HistogramAreaView:
struct HistogramView : View {
var title: String
var data: [ DataItem ]
var body: some View {
GeometryReader { gr in
let headHeight = gr. size. height * 0.10
VStack {
HistogramHeaderView ( title: title, height: headHeight)
HistogramAreaView ( data: data)
}
}
}
}
更新后的 HistogramView 需要一个 DataItem 的列表,GeometryReader 被用来确定条形图的可用高度,数据中的最大值得到后并传递给每个 ColumnView,主图表区域保持原来的圆角矩形,并以水平堆叠的方式叠加一系列条形,每个 DataItem 对应一个条形:
struct HistogramAreaView : View {
var data: [ DataItem ]
var body: some View {
GeometryReader { gr in
let fullBarHeight = gr. size. height * 0.90
let maxValue = data. map { $0 . value } . max ( ) !
ZStack {
RoundedRectangle ( cornerRadius: 5.0 )
. fill ( Color ( #colorLiteral ( red: 0.80 , green: 0.90 , blue: 0.80 , alpha: 1 ) ) )
VStack {
HStack ( spacing: 0 ) {
ForEach ( data) { item in
ColumnView (
name: item. name,
value: item. value,
maxValue: maxValue,
fullBarHeight: Double ( fullBarHeight) )
}
}
. padding ( 4 )
}
}
}
}
}
为 ColumnView 创建一个新的视图,该视图为每条数据创建一个条形图,它需要每一条数据的名称和值以及最大值和可用的条形高度,每个条形图都表示为圆角矩形,条形高度相对于最大条形高度设置,条形的颜色设置为红色:
struct ColumnView : View {
var name: String
var value: Double
var maxValue: Double
var fullBarHeight: Double
var body: some View {
let barHeight = ( Double ( fullBarHeight) / maxValue) * value
VStack {
Spacer ( )
ZStack {
VStack {
Spacer ( )
RoundedRectangle ( cornerRadius: 5.0 )
. fill ( Color . red)
. frame ( height: CGFloat ( barHeight) , alignment: . trailing)
}
VStack {
Spacer ( )
Text ( " \( value, specifier: "%.0F" ) " )
. font ( . footnote)
. foregroundColor ( . white)
. fontWeight ( . bold)
}
}
Text ( name) . font ( Font . system ( size: 13 ) )
}
. padding ( . horizontal, 4 )
}
}
三、横屏显示(屏幕旋转)
条形(柱状)图在使用样本数据时看起来不错,图表会调整到适合它所处的容器视图之中。同样的图表可以放到任何没有其他视图的新视图上,当设备旋转时,图标将会充满空间并调整大小:
struct ContentView : View {
let chartData: [ DataItem ] = [
DataItem ( name: "星期一" , value: 188 ) ,
DataItem ( name: "星期二" , value: 209 ) ,
DataItem ( name: "星期三" , value: 300 ) ,
DataItem ( name: "星期四" , value: 150 ) ,
DataItem ( name: "星期五" , value: 198 ) ,
DataItem ( name: "星期六" , value: 488 ) ,
DataItem ( name: "星期日" , value: 409 )
]
var body: some View {
VStack ( ) {
Text ( "屏幕使用时间" )
. font ( . title)
HistogramView (
title: "一周数据分析" , data: chartData)
Spacer ( )
}
. padding ( )
}
}
四、王者荣耀英雄胜率的柱状图渲染分析
王者荣耀里面有很多的英雄,根据版本的不同英雄的胜率也会有所不同,肯定有很多小伙伴儿好奇哪些英雄的胜率高,哪些英雄的胜率低? 现在我们就用条形图的形式来清晰的展示出 2022 王者荣耀英雄胜率排行。 2022 王者荣耀的打野路英雄的最新胜率,如下所示:
位置 热度 英雄 胜率 打野 T0 孙悟空 49.40% 打野 T1 李信 50.68% 打野 T1 亚瑟 48.41% 打野 T1 典韦 52.40% 打野 T1 凯 48.24% 打野 T1 李元芳 49.53% 打野 T1 澜 48.76% 打野 T2 兰陵王 47.08% 打野 T2 韩信 50.94% 打野 T2 赵云 50.34% 打野 T2 程咬金 50.47% 打野 T2 曜 51.80% 打野 T2 李白 50.49% 打野 T2 阿珂 50.77% 打野 T2 钟无艳 50.68% 打野 T2 宫本武藏 49.17% 打野 T3 芈月 50.17% 打野 T3 刘备 52.85% 打野 T3 娜可露露 48.39% 打野 T3 橘右京 49.69% 打野 T3 云樱 49.66% 打野 T3 镜 47.00% 打野 T3 司马懿 52.20% 打野 T3 露娜 49.88% 打野 T3 猪八戒 48.41% 打野 T3 阿骨朵 54.28% 打野 T3 暃 49.67% 打野 T3 达摩 49.85% 打野 T3 杨戬 52.44% 打野 T3 百里玄策 51.75% 打野 T3 曹操 48.91% 打野 T3 雅典娜 56.35% 打野 T3 裴擒虎 47.92% 打野 T3 云中君 50.68% 打野 T3 盘古 48.66%
struct DataItem : Identifiable {
let name: String
let value: Double
let id = UUID ( )
}
struct ContentView : View {
let chartData: [ DataItem ] = [
DataItem ( name: "孙悟空" , value: 49.40 ) ,
DataItem ( name: "李信" , value: 50.68 ) ,
DataItem ( name: "亚瑟" , value: 48.41 ) ,
DataItem ( name: "典韦" , value: 52.40 ) ,
DataItem ( name: "凯" , value: 48.24 ) ,
DataItem ( name: "李元芳" , value: 49.53 ) ,
DataItem ( name: "澜" , value: 48.76 ) ,
DataItem ( name: "兰陵王" , value: 47.08 ) ,
DataItem ( name: "韩信" , value: 50.94 ) ,
DataItem ( name: "赵云" , value: 50.34 ) ,
DataItem ( name: "程咬金" , value: 50.47 ) ,
DataItem ( name: "曜" , value: 51.80 ) ,
DataItem ( name: "李白" , value: 50.49 ) ,
DataItem ( name: "阿珂" , value: 50.77 ) ,
DataItem ( name: "钟无艳" , value: 50.68 ) ,
DataItem ( name: "宫本武藏" , value: 49.17 ) ,
DataItem ( name: "芈月" , value: 50.17 ) ,
DataItem ( name: "刘备" , value: 52.85 ) ,
DataItem ( name: "娜可露露" , value: 48.39 ) ,
DataItem ( name: "橘右京" , value: 49.69 ) ,
DataItem ( name: "云樱" , value: 49.66 ) ,
DataItem ( name: "镜" , value: 47.00 ) ,
DataItem ( name: "司马懿" , value: 52.20 ) ,
DataItem ( name: "露娜" , value: 49.88 ) ,
DataItem ( name: "猪八戒" , value: 48.41 ) ,
DataItem ( name: "阿骨朵" , value: 54.28 ) ,
DataItem ( name: "暃" , value: 49.67 ) ,
DataItem ( name: "达摩" , value: 49.85 ) ,
DataItem ( name: "杨戬" , value: 52.44 ) ,
DataItem ( name: "百里玄策" , value: 51.75 ) ,
DataItem ( name: "曹操" , value: 48.91 ) ,
DataItem ( name: "雅典娜" , value: 56.35 ) ,
DataItem ( name: "裴擒虎" , value: 47.92 ) ,
DataItem ( name: "云中君" , value: 50.68 ) ,
DataItem ( name: "盘古" , value: 48.66 )
]
var body: some View {
VStack ( ) {
Text ( "2022王者荣耀英雄胜率排行榜" )
. font ( . title)
HistogramView (
title: "打野路英雄胜率(%)" , data: chartData)
Spacer ( )
}
. padding ( )
}
}
对 HistogramView 做出了一些改动,条形图上的值使用叠加视图修改移到了条形图的顶部,这个值是偏移的,因此文本不会离条形图的顶部太近。数据名称的字体大小和字重也可以被设置,像部分英雄名称那样较长的文本,显示出条形图下面的文本将条形图推到了线外。文本视图的宽度被限制在条形图宽度的范围内,而且条形图的标签文本会被截断,条形图的文本视图也被限制在条形宽度的范围内,并且文本可以被隐藏起来:
struct HistogramView : View {
var title: String
var data: [ DataItem ]
var body: some View {
GeometryReader { gr in
let headHeight = gr. size. height * 0.10
VStack {
HistogramHeaderView ( title: title, height: headHeight)
HistogramAreaView ( data: data)
. frame ( width: CGFloat ( data. count) * 70 )
}
}
}
}
struct HistogramAreaView : View {
var data: [ DataItem ]
var body: some View {
GeometryReader { gr in
let fullBarHeight = gr. size. height * 0.90
let maxValue = data. map { $0 . value } . max ( ) !
ZStack {
RoundedRectangle ( cornerRadius: 5.0 )
. fill ( Color ( #colorLiteral ( red: 0.80 , green: 0.90 , blue: 0.80 , alpha: 1 ) ) )
VStack {
HStack ( spacing: 0 ) {
ForEach ( data) { item in
ColumnView (
name: item. name,
value: item. value,
maxValue: maxValue,
fullBarHeight: Double ( fullBarHeight) )
}
}
. padding ( 4 )
}
}
}
}
}
struct ColumnView : View {
var name: String
var value: Double
var maxValue: Double
var fullBarHeight: Double
var body: some View {
GeometryReader { gr in
let barHeight = ( Double ( fullBarHeight) / maxValue) * value
let textWidth = gr. size. width * 0.80
VStack {
Spacer ( )
RoundedRectangle ( cornerRadius: 6.0 )
. fill ( Color . red)
. frame ( width: 60 , height: CGFloat ( barHeight) , alignment: . trailing)
. overlay (
Text ( " \( value, specifier: "%.1f" ) " )
. font ( . footnote)
. foregroundColor ( . white)
. fontWeight ( . regular)
. frame ( width: textWidth)
. offset ( y: 8 )
,
alignment: . top
)
Text ( name)
. font ( . system ( size: 11 ) )
. fontWeight ( . semibold)
. lineLimit ( 5 )
. frame ( width: textWidth, alignment: . center)
}
. padding ( . horizontal, 0 )
}
}
}
将视图被嵌入到 ScrollView 中,以便在设备旋转时滚动:
struct ContentView : View {
let chartData: [ DataItem ] = [
DataItem ( name: "孙悟空" , value: 49.40 ) ,
DataItem ( name: "李信" , value: 50.68 ) ,
DataItem ( name: "亚瑟" , value: 48.41 ) ,
DataItem ( name: "典韦" , value: 52.40 ) ,
DataItem ( name: "凯" , value: 48.24 ) ,
DataItem ( name: "李元芳" , value: 49.53 ) ,
DataItem ( name: "澜" , value: 48.76 ) ,
DataItem ( name: "兰陵王" , value: 47.08 ) ,
DataItem ( name: "韩信" , value: 50.94 ) ,
DataItem ( name: "赵云" , value: 50.34 ) ,
DataItem ( name: "程咬金" , value: 50.47 ) ,
DataItem ( name: "曜" , value: 51.80 ) ,
DataItem ( name: "李白" , value: 50.49 ) ,
DataItem ( name: "阿珂" , value: 50.77 ) ,
DataItem ( name: "钟无艳" , value: 50.68 ) ,
DataItem ( name: "宫本武藏" , value: 49.17 ) ,
DataItem ( name: "芈月" , value: 50.17 ) ,
DataItem ( name: "刘备" , value: 52.85 ) ,
DataItem ( name: "娜可露露" , value: 48.39 ) ,
DataItem ( name: "橘右京" , value: 49.69 ) ,
DataItem ( name: "云樱" , value: 49.66 ) ,
DataItem ( name: "镜" , value: 47.00 ) ,
DataItem ( name: "司马懿" , value: 52.20 ) ,
DataItem ( name: "露娜" , value: 49.88 ) ,
DataItem ( name: "猪八戒" , value: 48.41 ) ,
DataItem ( name: "阿骨朵" , value: 54.28 ) ,
DataItem ( name: "暃" , value: 49.67 ) ,
DataItem ( name: "达摩" , value: 49.85 ) ,
DataItem ( name: "杨戬" , value: 52.44 ) ,
DataItem ( name: "百里玄策" , value: 51.75 ) ,
DataItem ( name: "曹操" , value: 48.91 ) ,
DataItem ( name: "雅典娜" , value: 56.35 ) ,
DataItem ( name: "裴擒虎" , value: 47.92 ) ,
DataItem ( name: "云中君" , value: 50.68 ) ,
DataItem ( name: "盘古" , value: 48.66 )
]
var body: some View {
ScrollView {
VStack ( ) {
Text ( "2022王者荣耀英雄胜率排行榜" )
. font ( . title)
Spacer ( ) . frame ( height: 20 )
Text ( "打野路英雄胜率(%)" )
. font ( . title3)
HistogramView (
title: "打野路英雄胜率(%)" , data: chartData)
. frame ( width: UIScreen . main. bounds. width - 60 , height: 280 , alignment: . center)
Spacer ( ) . frame ( height: 20 )
Spacer ( )
}
. padding ( )
}
}
}