Charts

Adding advanced visualisations to your apps just became a lot easier thanks to the new Charts library. Lets get to grips with it and look at how we can create beautiful animated charts with minimal efforts.

The basics

First, we need some data to map. I'm going to get started with a simple run model today, which we'll use to visualise our running distance.

struct Run: Identifiable, Hashable {
    var id = UUID()
    var distanceKm: Double
    var day: String

    internal init(id: UUID = UUID(), distanceKm: Double, day: String) {
        self.id = id
        self.distanceKm = distanceKm
        self.day = day
    }
}
struct Run: Identifiable, Hashable {
    var id = UUID()
    var distanceKm: Double
    var day: String

    internal init(id: UUID = UUID(), distanceKm: Double, day: String) {
        self.id = id
        self.distanceKm = distanceKm
        self.day = day
    }
}

Next, lets meet Chart.

As we've come to expect from SwiftUI, this is a really simple construct with some powerful options. Chart has an initilaiser that takes a trailing closure to build up its content. Lets add one without any content for now.

struct WeeklyDistanceView: View {
    @State var data: [Run] = [
        .init(distanceKm: 12.5, day: "Monday"),
        .init(distanceKm: 6.7, day: "Tuesday"),
        .init(distanceKm: 0, day: "Wednesday"),
        .init(distanceKm: 21.2, day: "Thursday"),
        .init(distanceKm: 10.4, day: "Friday"),
        .init(distanceKm: 4.3, day: "Saturday"),
        .init(distanceKm: 36.5, day: "Sunday"),
    ]

    
    var body: some View {
        Chart {
            // todo
        }
    }
}
struct WeeklyDistanceView: View {
    @State var data: [Run] = [
        .init(distanceKm: 12.5, day: "Monday"),
        .init(distanceKm: 6.7, day: "Tuesday"),
        .init(distanceKm: 0, day: "Wednesday"),
        .init(distanceKm: 21.2, day: "Thursday"),
        .init(distanceKm: 10.4, day: "Friday"),
        .init(distanceKm: 4.3, day: "Saturday"),
        .init(distanceKm: 36.5, day: "Sunday"),
    ]

    
    var body: some View {
        Chart {
            // todo
        }
    }
}

Data is added using Mark classes, which let you decide how you want to plot your data. To get started, we'll use BarMark to make a simple bar chart. This takes two arguments, the x and y value we want to plot. We're going to have our days along the bottom, and our distance along the side, so thats day for x and distance for y.

    var body: some View {
        Chart {
            ForEach(data) { (run) in
                BarMark(
                    x: .value("Date", run.day),
                    y: .value("Distance", run.distanceKm)
                )
                .foregroundStyle(.pink)
            }
        }
    }
    var body: some View {
        Chart {
            ForEach(data) { (run) in
                BarMark(
                    x: .value("Date", run.day),
                    y: .value("Distance", run.distanceKm)
                )
                .foregroundStyle(.pink)
            }
        }
    }

The chart we get as a result takes up all the available space, and nicely plots out our values for us. It's also pink.

simple-example

There's more options for us if we want them, including LineMark , AreaMark , PointMark and RectangleMark. Here's what they all look like.

simple-example simple-example simple-example simple-example

This shows how easy it is to re-use the same datasource for a whole bunch of different charts.

Getting fancy

Chart aren't limited to just one type, which means you can stack them atop one another.

For example, I might want to overlay my run data over the top of my sleep data, to check that im resting proportionally in-line with my effort levels.

To do this, you can simply stack content inside your Chart.

Here, i've added a sleepHours property to my run object, and used that to add a second chart over the top of my running distance.

    var body: some View {
        Chart {
            ForEach(data) { (run) in
                BarMark(
                    x: .value("Date", run.day),
                    y: .value("Distance", run.distanceKm)
                )
                .foregroundStyle(.pink)
            }
            ForEach(data) { run in
                AreaMark(
                    x: .value("Date", run.day),
                    y: .value("Distance", run.sleepHours)
                )
                .foregroundStyle(.blue.opacity(0.5))
            }
        }
        .frame(height: 200)
    }
    var body: some View {
        Chart {
            ForEach(data) { (run) in
                BarMark(
                    x: .value("Date", run.day),
                    y: .value("Distance", run.distanceKm)
                )
                .foregroundStyle(.pink)
            }
            ForEach(data) { run in
                AreaMark(
                    x: .value("Date", run.day),
                    y: .value("Distance", run.sleepHours)
                )
                .foregroundStyle(.blue.opacity(0.5))
            }
        }
        .frame(height: 200)
    }

simple-example

If the marks are the same, you get some special interactions between them. Here's the same example with two BarMarks, and you can see how it stacks nicely.

simple-example

Animating charts comes for free when you animate the data change, so lets take a look at how that could work.

Here I've got a chart of runs, and when I tap the plus button it adds another one. I can use implicit animations to tell SwiftUI I want any changes to animate

struct AnimatedChartsView: View {
    @State var data: [Run] = [
        .init(distanceKm: 12.5, day: "Monday"),
        .init(distanceKm: 6.7, day: "Tuesday"),
        .init(distanceKm: 0, day: "Wednesday"),
        .init(distanceKm: 21.2, day: "Thursday"),
        .init(distanceKm: 10.4, day: "Friday"),
        .init(distanceKm: 4.3, day: "Saturday")
    ]

    var body: some View {
        VStack {
            Chart {
                ForEach(data) { (run) in
                    BarMark(
                        x: .value("Date", run.day),
                        y: .value("Distance", run.distanceKm)
                    )
                    .foregroundStyle(.pink)
                }
            }
            .animation(.spring, value: data)
            .frame(height: 200)

            Button(action: {
                data.append(.init(distanceKm: Double.random(in: 0..<40), day: "Sunday"))
            }) {
                Label("Add your run", systemImage: "plus")
            }
        }
    }
}
struct AnimatedChartsView: View {
    @State var data: [Run] = [
        .init(distanceKm: 12.5, day: "Monday"),
        .init(distanceKm: 6.7, day: "Tuesday"),
        .init(distanceKm: 0, day: "Wednesday"),
        .init(distanceKm: 21.2, day: "Thursday"),
        .init(distanceKm: 10.4, day: "Friday"),
        .init(distanceKm: 4.3, day: "Saturday")
    ]

    var body: some View {
        VStack {
            Chart {
                ForEach(data) { (run) in
                    BarMark(
                        x: .value("Date", run.day),
                        y: .value("Distance", run.distanceKm)
                    )
                    .foregroundStyle(.pink)
                }
            }
            .animation(.spring, value: data)
            .frame(height: 200)

            Button(action: {
                data.append(.init(distanceKm: Double.random(in: 0..<40), day: "Sunday"))
            }) {
                Label("Add your run", systemImage: "plus")
            }
        }
    }
}

Here's how that feels.

simple-example

Notice what happens when I tap add repeatedly? Instead of adding new elements, SwiftUI is smart enough to manage this for me, and just add the new value to the existing "Sunday" rather than adding a new element along the bottom.

Making them pretty

This is an entirely optional step, but I think it shows how far you can go with these in a very short space of time.

A simple way to improve the visuals here is to wrap the charts in some nice boxes, give them some titles, and customise the colors a little, like this.

    var lineChart: some View {
        VStack(alignment: .leading) {
            Text("Lines")
                .font(.subheadline.weight(.semibold))
            Chart {
                ForEach(dataTwo) { (run) in
                    LineMark(
                        x: .value("Date", run.day),
                        y: .value("Distance", run.distanceKm)
                    )
                    .foregroundStyle(.blue)
                }
            }
            .frame(height: 200)
        }
    }
    var lineChart: some View {
        VStack(alignment: .leading) {
            Text("Lines")
                .font(.subheadline.weight(.semibold))
            Chart {
                ForEach(dataTwo) { (run) in
                    LineMark(
                        x: .value("Date", run.day),
                        y: .value("Distance", run.distanceKm)
                    )
                    .foregroundStyle(.blue)
                }
            }
            .frame(height: 200)
        }
    }

We've not done much, but this makes a big difference, espeically if you add lots of these together to make a big data view.

simple-example

And just for fun, lets randomise them with an animation.

simple-example

Finally, lets add a little more flair by using some gradients. We can use anything that you can use in foregroundStyle, so we can easily apply some lovely gradients.

    var barChart: some View {
        VStack(alignment: .leading) {
            Text("Bars")
                .font(.subheadline.weight(.semibold))
            Chart {
                ForEach(data) { (run) in
                    BarMark(
                        x: .value("Date", run.day),
                        y: .value("Distance", run.distanceKm)
                    )
                    .foregroundStyle(
                        .linearGradient(
                            colors: [.blue, .teal],
                            startPoint: .top,
                            endPoint: .bottom
                        )
                    )
                }
            }
            .frame(height: 200)
        }
    }
    var barChart: some View {
        VStack(alignment: .leading) {
            Text("Bars")
                .font(.subheadline.weight(.semibold))
            Chart {
                ForEach(data) { (run) in
                    BarMark(
                        x: .value("Date", run.day),
                        y: .value("Distance", run.distanceKm)
                    )
                    .foregroundStyle(
                        .linearGradient(
                            colors: [.blue, .teal],
                            startPoint: .top,
                            endPoint: .bottom
                        )
                    )
                }
            }
            .frame(height: 200)
        }
    }

simple-example


Thanks for reading!

You can find my code on github. The good stuff is in WeeklyChartsView.