How to create a bar chart in SwiftUI

How to create a bar chart in SwiftUI

Introduction

A bar chart presents categories of data in the form of rectangular bars whose width and height are proportional to the values ​​they represent. This article will show you how to create a vertical bar chart where the height of the rectangle will represent the value of each category.

Start chart layout

SwiftUI is very friendly for exploring different layouts and previewing the results in a live view. It’s easy to extract parts of the content into subviews so that each part is small and easy to maintain. Start with a view that will contain the BarChartView​ and possibly other text or data. This BarChartView contains a title and a chart area, which are represented by text and rounded rectangles.

 struct ChartView1 : View {
var body : some View {
VStack {
Text ( "Sample Bar Chart" )
.font ( .title )

BarChartView (
title : "the chart title" )
.frame ( width : 300 , height : 300 , alignment : .center )

Spacer ( )
}
}
}
 struct BarChartView: View {
var title: String

var body: some View {
GeometryReader { gr in
let headHeight = gr.size.height * 0.10
VStack {
ChartHeaderView(title: title, height: headHeight)
ChartAreaView()
}
}
}
}
 struct ChartHeaderView : View {
var title : String
var height : CGFloat

var body : some View {
Text ( title )
.frame ( height : height )
}
}
 struct ChartAreaView : View {
var body : some View {
ZStack {
RoundedRectangle ( cornerRadius : 5.0 )
.fill ( Color ( #colorLiteral ( red : 0.8906477705 , green : 0.9005050659 , blue : 0.8208766097 , alpha : 1 ) ) )
}
}
}

Add a bar chart to the chart area

Define some simple data categories, such as the number of steps per day in a week. The following list data is used as the item data of the main view, and each piece of data contains a pair (name, value). In a real app, the data here should be taken from the model through ViewModel.

Daily step count data

Day

Steps

Mon

898

Tue

670

Wed

725

Thu

439

Fri

1232

Sat

771

Sun

365

 struct DataItem : Identifiable {
let name : String
let value : Double
let id = UUID ( )
}

struct ChartView2 : View {

let chartData : [ DataItem ] = [
DataItem ( name : "Mon" , value : 898 ) ,
DataItem ( name : "Tue" , value : 670 ) ,
DataItem ( name : "Wed" , value : 725 ) ,
DataItem ( name : "Thu" , value : 439 ) ,
DataItem ( name : "Fri" , value : 1232 ) ,
DataItem ( name : "Sat" , value : 771 ) ,
DataItem ( name : "Sun" , value : 365 )
]

var body : some View {
VStack {
Text ( "Sample Bar Chart" )
.font ( .title )

BarChartView (
title : "Daily step count" , data : chartData )
.frame ( width : 350 , height : 500 , alignment : .center )

Spacer ( )
}
}
}

Updated BarChartView​ so data can be passed as a parameter to ChartAreaView

 struct BarChartView : View {
var title : String
var data : [ DataItem ]

var body : some View {
GeometryReader { gr in
let headHeight = gr .size .height * 0.10
VStack {
ChartHeaderView ( title : title , height : headHeight )
ChartAreaView ( data : data )
}
}
}
}

The updated BarChartView​ requires a list of DataItem​ . A GeometryReader is used to determine the available heights for the bars. The maximum value in the data is obtained and passed to each BarView​. The main chart area remains the original rounded rectangle and overlays a series of bars in a horizontal stack, one for each DataItem .

 struct ChartAreaView : 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.8906477705 , green : 0.9005050659 , blue : 0.8208766097 , alpha : 1 ) ) )

VStack {
HStack ( spacing : 0 ) {
ForEach ( data ) { item in
BarView (
name : item .name ,
value : item .value ,
maxValue : maxValue ,
fullBarHeight : Double ( fullBarHeight ) )
}
}
.padding ( 4 )
}

}
}
}
}

A new view is created for the BarView that creates a bar for each piece of data. It requires the name and value of each piece of data as well as the maximum and available bar height. Each bar is represented as a rounded rectangle, and the bar height is set relative to the maximum bar height. The color of the bar is set to solid blue.

 struct BarView : 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 .blue )
.frame ( height : CGFloat ( barHeight ) , alignment : .trailing )
}

VStack {
Spacer ( )
Text ( "\(value, specifier: " % .0F ")" )
.font ( .footnote )
.foregroundColor ( .white )
.fontWeight ( .bold )
}
}
Text ( name )
}
.padding ( .horizontal , 4 )
}
}

Screen rotation

The bar chart looks great with sample data. The chart will resize to fit the container view it is in. The same chart can be dropped into any new view without other views, and the icon will fill the space and resize when the device is rotated.

 struct ChartView3 : View {
var body : some View {
VStack ( ) {

BarChartView (
title : "Daily step count" , data : chartData )

Spacer ( )
}
.padding ( )
}
}

Chart displayed when the phone is rotated

Bar chart of real data

Use real-world data for the bar chart. The ten countries with the highest under-five mortality rates from the UNICEF dataset.

Under-five mortality rate:

The probability of dying between birth and five years of age, per 1,000 live births

Country-selected estimates of under-five mortality rates in 2019

ISO Code

Country Name

2019

NGA

Nigeria

117.2

SOM

Somalia

116.9

TCD

Chad

113.7

CAF

Central African Republic

110.0

SLE

Sierra Leone

109.2

GIN

Guinea

98.8

SSD

South Sudan

96.2

MLI

Mali

94.0

BEN

Benin

90.2

BFA

Burkina Faso

87.5

LSO

Lesotho

86.4

As can be seen, the country names are much longer than the days of the week in the sample data, which uses multiple data names. The data is plotted in a bar chart using the country names.

 struct ChartView4 : View {
let chartData : [ DataItem ] = [
DataItem ( name : "Nigeria" , value : 117.2 ) ,
DataItem ( name : "Somalia" , value : 116.9 ) ,
DataItem ( name : "Chad" , value : 113.7 ) ,
DataItem ( name : "Central African Republic" , value : 110.0 ) ,
DataItem ( name : "Sierra Leone" , value : 109.2 ) ,
DataItem ( name : "Guinea" , value : 98.8 ) ,
DataItem ( name : "South Sudan" , value : 96.2 ) ,
DataItem ( name : "Mali" , value : 94.0 ) ,
DataItem ( name : "Benin" , value : 90.2 ) ,
DataItem ( name : "Burkina Faso" , value : 87.5 )
]

var body : some View {
VStack ( ) {

BarChartView (
title : "Under Five Mortality Rates in 2019" , data : chartData )
.frame ( width : 350 , height : 500 , alignment : .center )

Text ( "Under-five mortality rate:" )
Text ( "is the probability of dying between birth and exactly 5 years of age, expressed per 1,000 live births." )

Spacer ( )
}
.padding ( )
}
}

Here are some changes made to the BarView. The value on the bar is moved to the top of the bar using an overlay view. The value is offset so the text is not too close to the top of the bar. The font size and weight of the data name can also be set. For longer text like the country names, showing the text below the bar pushes the bar outside the line. The text view width is constrained to the bar width and the bar label text is truncated. The bar text view is also constrained to the bar width and the text can be hidden.

 struct BarView : 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 : 5.0 )
.fill ( Color .blue )
.frame ( height : CGFloat ( barHeight ) , alignment : .trailing )
.overlay (
Text ( "\(value, specifier: " % .0F ")" )
.font ( .footnote )
.foregroundColor ( .white )
.fontWeight ( .bold )
.frame ( width : textWidth )
.offset ( y : 10 )
,
alignment : .top
)

Text ( name )
.font ( .system ( size : 11 ) )
.fontWeight ( .semibold )
.lineLimit ( 1 )
.frame ( width : textWidth )
}
.padding ( .horizontal , 4 )
}
}
}

All country names are truncated, so the data is changed to use country codes instead of country names. The icons are set to a fixed size and the view is embedded in a ScrollView so that it scrolls when the device is rotated.

 struct ChartView5 : View {
let chartData : [ DataItem ] = [
DataItem ( name : "NGA" , value : 117.2 ) ,
DataItem ( name : "SOM" , value : 116.9 ) ,
DataItem ( name : "TCD" , value : 113.7 ) ,
DataItem ( name : "CAF" , value : 110.0 ) ,
DataItem ( name : "SLE" , value : 109.2 ) ,
DataItem ( name : "GIN" , value : 98.8 ) ,
DataItem ( name : "SSD" , value : 96.2 ) ,
DataItem ( name : "MLI" , value : 94.0 ) ,
DataItem ( name : "BEN" , value : 90.2 ) ,
DataItem ( name : "BFA" , value : 87.5 )
]

var body : some View {
ScrollView {
VStack ( ) {

BarChartView (
title : "Countries with the highest Under Five Mortality Rates in 2019" , data : chartData )
.frame ( width : 350 , height : 500 , alignment : .center )

Spacer ( ) .frame ( height : 20 )

VStack ( ) {
Text ( "Under-five mortality rate:" )
.font ( .system ( .title2 , design :. rounded ) )
.fontWeight ( .bold )
Text ( "is the probability of dying between birth and exactly 5 years of age, expressed per 1,000 live births." )
.font ( .body )
}
.frame ( width : 300 , height : 130 )
.background ( Color ( #colorLiteral ( red : 0.8906477705 , green : 0.9005050659 , blue : 0.8208766097 , alpha : 1 ) ) )
.cornerRadius ( 10 )

Spacer ( )
}
.padding ( )
}
}
}

Conclusion

It's relatively easy to combine rectangles to create a bar chart in SwiftUI. SwiftUI is a great platform for creating views and quickly refactoring independent subviews. Building a bar chart in SwiftUI requires some work, and as you experiment with bar charts using data, you can determine more customization. Using GeometryReader, you can create bar charts that adapt to more available environments. In this post, we created a simple bar chart with values, labels below, and a title for the chart. The next step is to separate the x-axis and y-axis.

<<:  The official version of iOS 16.1.1 will be released soon, fixing multiple issues!

>>:  iOS 16.2 Beta 2 updated with several changes!

Recommend

Can WeChat be used like this? Hidden WeChat operations that will open your eyes

WeChat is used every day, but do you know how man...

5 key points for beginners to do Tik Tok live streaming!

On April 15, "Overseas Flight Attendant Ling...

4 counter-common sense suggestions for marketing, branding and markets

Just as there are a thousand Hamlets in the eyes ...

How to operate Toutiao account? New to operating a Toutiao account?

Toutiao is one of the important platforms of self...

Dong Mingzhu said Gree mobile phone is on sale, why can’t we find it?

The furthest distance in the world is not that yo...

Why do mother penguins "abandon" the first egg they lay?

Produced by: Science Popularization China Author:...

IOS high imitation storm video player app source code

Source code introduction: This is a demo modeled ...

What is your mobile phone number "bound" to? Come and check it out

In this era where we can connect to the world wit...