New in SwiftUI 4: Charts (Bar chart)

DevTechie Inc
Jun 24

With the release of SwiftUI 4, Apple has made it super simple to add charts to your apps. With a simple Charts framework import and a simple API, we get powerful charts out of the box.

Today, we will look at creating a bar chart in SwiftUI 4.

Basic Bar Chart
Let’s start by putting together a simple chart view. We will start by importing Charts framework and creating a Chart view.

Note: Chart is a SwiftUI view that displays a chart
import SwiftUI
import Chartsstruct ContentView: View {
    var body: some View {
        Chart {
            // TODO add Marks
        }
    }
}
Adding Bars with BarMark
Inside the chart, we specify the graphical marks that represent data which needs to be plotted. We will start by adding a single BarMark inside our chart.

Note: BarMark represents data using bars. BarMark takes x and y values, these values are PlottableValue types. PlottableValue type conforms to Plottable protocol which is a type that can serve as data for the labeled, plottable values that we can draw on a chart.
We will plot daily workout sessions inside our chart example, so let’s add a BarMark for Monday with time spent on workout as 20 minutes.

struct ContentView: View {
    var body: some View {
        VStack {
            Text("DevTechie.com")
                .font(.largeTitle)
            Chart {
                BarMark(
                    x: .value("Day", "Mon"),
                    y: .value("Min", 20)
                )
            }
        }
    }
}
As we can see our Chart view takes over the screen and plots a single bar for the available space. We can use existing modifiers to style our charts. Let’s set the height for the chart using the frame modifier.

struct ContentView: View {
    var body: some View {
        VStack {
            Text("DevTechie.com")
                .font(.largeTitle)
            Chart {
                BarMark(
                    x: .value("Day", "Mon"),
                    y: .value("Min", 20)
                )
            }
            .frame(height: 400)
        }
    }
}
Let’s add workout sessions for two more days:

struct ContentView: View {
    var body: some View {
        VStack {
            Text("DevTechie.com")
                .font(.largeTitle)
            Chart {
                BarMark(
                    x: .value("Day", "Mon"),
                    y: .value("Min", 20)
                )
                BarMark(
                    x: .value("Day", "Tue"),
                    y: .value("Min", 65)
                )
                BarMark(
                    x: .value("Day", "Wed"),
                    y: .value("Min", 45)
                )
            }
            .frame(height: 400)
        }
    }
}
Nominal vs Quantitative Data
Notice how our chart view adjusts to make room to plot newly added data into the view. Also, all the bars are scaled aptly.

In our chart, we have two types of data

  • Nominal data: this is the data used for label, i.e day of the week in our example
  • Quantitative data: this is a measure of values or counts and are expressed as numbers, i.e number of minutes spent in workout in our example.
In our example, we are plotting nominal data on the x-axis and quantitative data on the y-axis:

BarMark(
    x: .value("Day", "Mon"),
    y: .value("Min", 20)
)
If we flip these and change them to show nominal data on the y-axis and quantitative data on the x-axis:

BarMark(
    x: .value("Min", 20),
    y: .value("Day", "Mon")
)
We get horizontal bar chart.

struct ContentView: View {
    var body: some View {
        VStack {
            Text("DevTechie.com")
                .font(.largeTitle)
            Chart {
                BarMark(
                    x: .value("Min", 20),
                    y: .value("Day", "Mon")
                )
                BarMark(
                    x: .value("Min", 65),
                    y: .value("Day", "Tue")
                )
                BarMark(
                    x: .value("Min", 45),
                    y: .value("Day", "Wed")
                )
            }
            .frame(height: 400)
        }
    }
}
Dynamic Bar Chart
So far, we have been creating data marks inside the chart view, but in a production environment this will come dynamically, so let’s change our code a bit.

We will create a simple data structure to use with our shiny new chart. ✨. Let’s create a struct to capture total minutes spent on workouts by day.

struct Workout: Identifiable {
    var id = UUID()
    var day: String
    var minutes: Double
}
We will add some sample data inside the extension for this workout struct, as shown below:

extension Workout {
    static let workouts: [Workout] = [
        Workout(day: "Mon", minutes: 32),
        Workout(day: "Tue", minutes: 45),
        Workout(day: "Wed", minutes: 56),
        Workout(day: "Thu", minutes: 15),
        Workout(day: "Fri", minutes: 65),
        Workout(day: "Sat", minutes: 8),
        Workout(day: "Sun", minutes: 10),
    ]
}
Once we have our data structure ready, we just plug it in as shown below:

import SwiftUI
import Chartsstruct ContentView: View {
    var body: some View {
        VStack {
            Text("DevTechie.com")
                .font(.largeTitle)
            Chart(Workout.workouts) { workout in
                BarMark(
                    x: .value("Day", workout.day),
                    y: .value("Workout(In minutes)", workout.minutes))
            }
            .frame(height: 400)
        }
        .padding()
    }
}struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}struct Workout: Identifiable {
    var id = UUID()
    var day: String
    var minutes: Double
}extension Workout {
    static let workouts: [Workout] = [
        Workout(day: "Mon", minutes: 32),
        Workout(day: "Tue", minutes: 45),
        Workout(day: "Wed", minutes: 56),
        Workout(day: "Thu", minutes: 15),
        Workout(day: "Fri", minutes: 65),
        Workout(day: "Sat", minutes: 8),
        Workout(day: "Sun", minutes: 10),
    ]
}
We can easily swap the values of x and y to show horizontal bar:

struct ContentView: View {
    var body: some View {
        VStack {
            Text("DevTechie.com")
                .font(.largeTitle)
            Chart(Workout.workouts) { workout in
                BarMark(
                    x: .value("Workout(In minutes)", workout.minutes),
                    y: .value("Day", workout.day))
            }
            .frame(height: 400)
        }
        .padding()
    }
}
Coloring the Bars
We can change color of the bar by using foregroundStyle modifier.

struct ContentView: View {
    var body: some View {
        VStack {
            Text("DevTechie.com")
                .font(.largeTitle)
            Chart(Workout.workouts) { workout in
                BarMark(
                    x: .value("Workout(In minutes)", workout.minutes),
                    y: .value("Day", workout.day))
                .foregroundStyle(.pink)
            }
            .frame(height: 400)
        }
        .padding()
    }
}
We can use gradient as well:

struct ContentView: View {
    var body: some View {
        VStack {
            Text("DevTechie.com")
                .font(.largeTitle)
            Chart(Workout.workouts) { workout in
                BarMark(
                    x: .value("Workout(In minutes)", workout.minutes),
                    y: .value("Day", workout.day))
                .foregroundStyle(.linearGradient(colors: [.orange, .pink], startPoint: .leading, endPoint: .trailing))
            }
            .frame(height: 400)
        }
        .padding()
    }
}
Notice how we have all the bars of the same color. This is great, but what if we need to color each bar a different color? Well, that can be done with another overload of the foregroundStyle modifier, where it takes PlottableValue as a parameter. With this new modifier, SwiftUI picks colors for you and colors each bar so you can identify them easily.

struct ContentView: View {
    var body: some View {
        VStack {
            Text("DevTechie.com")
                .font(.largeTitle)
            Chart(Workout.workouts) { workout in
                BarMark(
                    x: .value("Workout(In minutes)", workout.minutes),
                    y: .value("Day", workout.day))
                .foregroundStyle(by: .value("Day", workout.day))
            }
            .frame(height: 400)
        }
        .padding()
    }
}
If you are a keen observer, you will notice something new on this screen. SwiftUI automatically adds a new view on to the screen called Legend.

Chart legend appears by default when you first create a chart with a different foreground style. Legends show the names and colors of each series of data. The legend text is taken from the chart’s nominal value.

Chart Foreground Style Scale
Applying foregroundStyle(by: PlottableValue) presents a challenge where we lose the ability to add color of our choice. Charts API does a great job picking random colors for us, but if we want more control over the theme, we can change that with the help of chartForegroundStyleScale modifier.

chartForegroundStyleScale modifier configures the foreground style scale for charts and using its domain and range parameters we can add our own colors to the bar.

chartForegroundStyleScale function takes 3 parameters:

domain: The possible data values plotted as foreground style in the chart. You can define the domain with a ClosedRange for number or Date values (e.g., 0 ... 500), and with an array for categorical values (e.g., ["A", "B", "C"])

range: The range of foreground styles that correspond to the scale domain.

scale: scale type defines the ways you can scale the domain or range of a plot. Default value is nil.

Let’s use this with our own set of colors:

struct ContentView: View {
    let markColors: [Color] = [.pink, .purple, .cyan, .brown, .orange, .blue, .mint]
    var body: some View {
        VStack {
            Text("DevTechie.com")
                .font(.largeTitle)
            Chart(Workout.workouts) { workout in
                BarMark(
                    x: .value("Workout(In minutes)", workout.minutes),
                    y: .value("Day", workout.day))
                .foregroundStyle(by: .value("Day", workout.day))
            }
            .chartForegroundStyleScale(domain: Workout.workouts.compactMap({ workout in
                workout.day
            }), range: markColors)
            .frame(height: 400)
        }
        .padding()
    }
}
We can use array of gradient colors as well:

struct ContentView: View {
    let markColors: [LinearGradient] = [
            LinearGradient(colors: [.pink, .green], startPoint: .leading, endPoint: .trailing),
            LinearGradient(colors: [.blue, .green], startPoint: .leading, endPoint: .trailing),
            LinearGradient(colors: [.orange, .green], startPoint: .leading, endPoint: .trailing),
            LinearGradient(colors: [.mint, .green], startPoint: .leading, endPoint: .trailing),
            LinearGradient(colors: [.cyan, .green], startPoint: .leading, endPoint: .trailing),
            LinearGradient(colors: [.purple, .green], startPoint: .leading, endPoint: .trailing),
            LinearGradient(colors: [.indigo, .green], startPoint: .leading, endPoint: .trailing),
        ]
    var body: some View {
        VStack {
            Text("DevTechie.com")
                .font(.largeTitle)
            Chart(Workout.workouts) { workout in
                BarMark(
                    x: .value("Workout(In minutes)", workout.minutes),
                    y: .value("Day", workout.day))
                .foregroundStyle(by: .value("Day", workout.day))
            }
            .chartForegroundStyleScale(domain: Workout.workouts.compactMap({ workout in
                workout.day
            }), range: markColors)
            .frame(height: 400)
        }
        .padding()
    }
}
Annotations
Marks can be decorated with annotations to show relevant information. Annotation is a SwiftUI view meaning, it’s on us to how we would like to show the information which is relevant and pleasant for the user.

We will annotate our bar marks with a simple indigo color figure image with the help of the annotation modifier, which takes position as a parameter to define the location where the annotation will be shown:

struct ContentView: View {
    let markColors: [LinearGradient] = [
        LinearGradient(colors: [.pink, .green], startPoint: .leading, endPoint: .trailing),
        LinearGradient(colors: [.blue, .green], startPoint: .leading, endPoint: .trailing),
        LinearGradient(colors: [.orange, .green], startPoint: .leading, endPoint: .trailing),
        LinearGradient(colors: [.mint, .green], startPoint: .leading, endPoint: .trailing),
        LinearGradient(colors: [.cyan, .green], startPoint: .leading, endPoint: .trailing),
        LinearGradient(colors: [.purple, .green], startPoint: .leading, endPoint: .trailing),
        LinearGradient(colors: [.indigo, .green], startPoint: .leading, endPoint: .trailing),
    ]
    var body: some View {
        VStack {
            Text("DevTechie.com")
                .font(.largeTitle)
            Chart(Workout.workouts) { workout in
                BarMark(
                    x: .value("Workout(In minutes)", workout.minutes),
                    y: .value("Day", workout.day))
                .foregroundStyle(by: .value("Day", workout.day))
                .annotation(position: .trailing) {
                    Image(systemName: "figure.stand")
                        .foregroundColor(.indigo)
                }
            }
            .chartForegroundStyleScale(domain: Workout.workouts.compactMap({ workout in
                workout.day
            }), range: markColors)
            .frame(height: 400)
        }
        .padding()
    }
}
We can take this a step further by building a custom view just for annotations. For that, we will move code for colors into its own struct in order to access it from other views.

struct Constants {
    static let markColors: [LinearGradient] = [
        LinearGradient(colors: [.pink, .green], startPoint: .leading, endPoint: .trailing),
        LinearGradient(colors: [.blue, .green], startPoint: .leading, endPoint: .trailing),
        LinearGradient(colors: [.orange, .green], startPoint: .leading, endPoint: .trailing),
        LinearGradient(colors: [.mint, .green], startPoint: .leading, endPoint: .trailing),
        LinearGradient(colors: [.cyan, .green], startPoint: .leading, endPoint: .trailing),
        LinearGradient(colors: [.purple, .green], startPoint: .leading, endPoint: .trailing),
        LinearGradient(colors: [.indigo, .green], startPoint: .leading, endPoint: .trailing),
    ]
}
Next, we will create AnnotationView which will take workout as a parameter, as shown below:

struct AnnotationView: View {
    let workout: Workout
    
    var body: some View {
        let idx = Workout.workouts.firstIndex(where: {$0.id == workout.id}) ?? 0
        return VStack(spacing: 0) {
            Text("\(workout.minutes.formatted()) mins")
            Image(systemName: "figure.stand")
        }
        .font(.caption)
        .foregroundStyle(Constants.markColors[idx])
    }
}
Now, we can use the new view and constants struct inside our ContentView:

struct ContentView: View {
    
    var body: some View {
        VStack {
            Text("DevTechie.com")
                .font(.largeTitle)
            Chart(Workout.workouts) { workout in
                BarMark(
                    x: .value("Workout(In minutes)", workout.minutes),
                    y: .value("Day", workout.day))
                .foregroundStyle(by: .value("Day", workout.day))
                .annotation(position: .trailing) {
                    AnnotationView(workout: workout)
                }
            }
            .chartForegroundStyleScale(domain: Workout.workouts.compactMap({ workout in
                workout.day
            }), range: Constants.markColors)
            .frame(height: 400)
        }
        .padding()
    }
}
We can align annotation to the top of bar marks for vertical bar chart as shown below:

struct ContentView: View {
    
    var body: some View {
        VStack {
            Text("DevTechie.com")
                .font(.largeTitle)
            Chart(Workout.workouts) { workout in
                BarMark(
                    x: .value("Day", workout.day),
                    y: .value("Workout(In minutes)", workout.minutes))
                .foregroundStyle(by: .value("Day", workout.day))
                .annotation(position: .top) {
                    AnnotationView(workout: workout)
                }
            }
            .chartForegroundStyleScale(domain: Workout.workouts.compactMap({ workout in
                workout.day
            }), range: Constants.markColors)
            .frame(height: 400)
        }
        .padding()
    }
}


With that we have reached the end of this article. Thank you once again for reading. Subscribe to our weekly newsletter at https://www.devtechie.com