Thanks for being a part of WWDC25!

How did we do? We’d love to know your thoughts on this year’s conference. Take the survey here

Swift Charts - weak scrolling performance

Hello there!

I wanted to give a native scrolling mechanism for the Swift Charts Graph a try and experiment a bit if the scenario that we try to achieve might be possible, but it seems that the Swift Charts scrolling performance is very poor.

The graph was created as follows:

  • X-axis is created based on a date range,
  • Y-axis is created based on an integer values between moreless 0-320 value.
  • the graph is scrollable horizontally only (x-axis),
  • The time range (x-axis) for the scrolling content was set to one year from now date (so the user can scroll one year into the past as a minimum visible date (.chartXScale).
  • The X-axis shows 3 hours of data per screen width (.chartXVisibleDomain).
  • The data points for the graph are generated once when screen is about to appear so that the Charts engine can use it (no lazy loading implemented yet).
  • The line data points (LineMark views) consist of 2880 data points distributed every 5 minutes which simulates - two days of continuous data stream that we want to present. The rest of the graph displays no data at all.

The performance result:

  • The graph on the initial loading phase is frozen for about 10-15 seconds until the data appears on the graph.
  • Scrolling is very laggy - the CPU usage is 100% and is unacceptable for the end users.
  • If we show no data at all on the graph (so no LineMark views are created at all) - the result is similar - the empty graph scrolling is also very laggy.

Below I am sharing a test code:

@main
struct ChartsTestApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
            Spacer()
        }
    }
}

struct LineDataPoint: Identifiable, Equatable {
    var id: Int
    let date: Date
    let value: Int
}

actor TestData {
    func generate(startDate: Date) async -> [LineDataPoint] {
        var values: [LineDataPoint] = []
        
        for i in 0..<(1440 * 2) {
            values.append(
                LineDataPoint(
                    id: i,
                    date: startDate.addingTimeInterval(
                        TimeInterval(60 * 5 * i) // Every 5 minutes
                    ),
                    value: Int.random(in: 1...100)
                )
            )
        }
        
       return values
    }
}

struct ContentView: View {
    var startDate: Date {
        return endDate.addingTimeInterval(-3600*24*30*12) // one year into the past from now
    }
    let endDate = Date()
    
    @State var dataPoints: [LineDataPoint] = []
    
    var body: some View {
        Chart {
            ForEach(dataPoints) { item in
                LineMark(
                    x: .value("Date", item.date),
                    y: .value("Value", item.value),
                    series: .value("Series", "Test")
                )
            }
        }
        .frame(height: 200)
        .chartScrollableAxes(.horizontal)
        .chartYAxis(.hidden)
        .chartXScale(domain: startDate...endDate) // one year possibility to scroll back
        .chartXVisibleDomain(length: 3600 * 3) // 3 hours visible on screen
        .onAppear {
            Task {
                dataPoints = await TestData().generate(startDate: startDate)
            }
        }
    }
}

I would be grateful for any insights or suggestions on how to improve it or if it's planned to be improved in the future.

Currently, I use UIKit CollectionView where we split the graph into smaller chunks of the graph and we present the SwiftUI Chart content in the cells, so we use the scrolling offered there. I wonder if it's possible to use native SwiftUI for such a scenario so that later on we could also implement some kind of lazy loading of the data as the user scrolls into the past.

Swift Charts is incapable of plotting 2k data points and handling animation even with the vectorized Line plot API. Trying to do this will cause microhangs or hangs.

Late response. I am also experiencing this, where I try to plot scientific data with thousands of points. Intitial drawing of a plot is slow, after that it is a tad better.

Two things I have found:

  • .chartXScale and .chartXVisibleDomain don't work well together, and I got rid of .chartXScale.
  • Setting a really high initial value for .chartXVisibleDomain improved the lagging.

But it still lags. Seems like the framework is not really intended for large data sets.

Processing lots of data points takes resources. Swift Charts works with RandomAccessCollections, for example in the ForEach(data) { ... }, or its Chart(data) { ... } shorthand. Using the ever current .chartScrollPosition(x: $position) as the lower bound, and that plus the specified visible domain as the upper bound, you can partition your data (eg. with binary search, if your data is ordered; Swift Algorithms has partitioningIndex(where:) and reversePartitioningIndex(where:) to help establish the beginning and end of your data slice) to only include the part of data that gets rendered. Add some padding on either side so partially visible bars also show up.

Similarly, the axis tick values can be specified for the ever current visible window. AxisMarks lets you specify the tick values directly. Also, the collision resolution can be disabled, if you made sure the distance between ticks is always enough for tick labels.

.chartXAxis {
    // The axis ticks are restricted to approx. the visible window, because axis generation is currently expensive with lots of ticks.
    // It has to include ticks that may be outside the visible domain, but whose tick labels may be visible.
    let from = /* scroll position minus the length passed to `chartXVisibleDomain` */
    let to = /* scroll position plus one bar width plus the length passed to `chartXVisibleDomain */
    AxisMarks(preset: .aligned, values:  Array(/* compute from the `from` and `to` */) { value in
        AxisTick(...)

        if isLabeled {
            AxisValueLabel(anchor: .topLeading, collisionResolution: .disabled, verticalSpacing: 8) {
                Text(...)
            }
        }
    }
}
Swift Charts - weak scrolling performance
 
 
Q