SwiftUI bar charts shows overlapping bars

I'm doing a dead simple bar chart. It will show one bar per hour for a few days. The bar chart seems to have a bug where it will overlap the bars as soon as the overall width of the chart reaches some hardcoded value. The chart has .chartScrollableAxes(.horizontal) so horizontal space is no issue, there's infinite amounts of it.

This screenshot shows the same content, but the bottom one has 25pt wide bars and the top one 16pt. 16 is the last width before they started overlapping.

To test play with numberOfValues as well as the fixed widths for the BarMark:s. Under no circumstances should the bars overlap unless I tell it to, it should use some configurable minimum spacing between bars. In my real case I do not artificially color the bars like this and the chart is really hard to read.

I've tried to look in the docs, but most modifiers are totally undocumented and I can't seem to find anything that would apply. By setting .chartXVisibleDomain to some really low value I can force it to spread the bars out more, but then the my bar width is not honoured.

import SwiftUI
import Charts

struct Value: Hashable {
    let x: Int
    let y: Int
}

struct ContentView: View {
    let values: [Value]
    let colors: [Color] = [.red, .green, .blue]
    
    var body: some View {
        VStack {
            Chart {
                ForEach(values, id: \.self) { value in
                    BarMark(
                        x: .value("X", value.x),
                        y: .value("Y", value.y),
                        width: .fixed(16)
                    )
                    .foregroundStyle(colors[value.x % colors.count])
                }
            }
            .chartScrollableAxes(.horizontal)

            Chart {
                ForEach(values, id: \.self) { value in
                    BarMark(
                        x: .value("X", value.x),
                        y: .value("Y", value.y),
                        width: .fixed(25)
                    )
                    .foregroundStyle(colors[value.x % colors.count])
                }
            }
            .chartScrollableAxes(.horizontal)
        }
        
        .padding()
    }
}

#Preview {
    var rnd = SystemRandomNumberGenerator()
    let numberOfValues = 50
    var values: [Value] = []
    for index in 0 ..< numberOfValues {
        values.append(Value(x: index, y: Int(rnd.next() % 50)))
    }
    
    return ContentView(values: values)
}

Example with bars the same color. It's pretty much unusable.

Replies

I added chartXVisibleDomain to make it work.

            Chart {
                ForEach(values, id: \.self) { value in
                    BarMark(
                        x: .value("X", value.x),
                        y: .value("Y", value.y),
                        width: .fixed(25)
                    )
                    .foregroundStyle(colors[value.x % colors.count])
                }
            }
            .chartScrollableAxes(.horizontal)
            .chartXVisibleDomain(length: 12) // You could compute the value instead of 12, depending on screen and bar mark width

To automatically compute it:

struct ContentView: View {
    @State var values: [Value] = [] // Initial value, will be computed in onAppear
    @State var chartLength = 12.    // Initial value, will be computed in onAppear
    let colors: [Color] = [.red, .green, .blue]
    let barWidth : CGFloat = 25
    
    var body: some View {
        VStack {
            Chart {
                ForEach(values, id: \.self) { value in
                    BarMark(
                        x: .value("X", value.x),
                        y: .value("Y", value.y),
                        width: .fixed(16)
                    )
                    .foregroundStyle(colors[value.x % colors.count])
                }
            }
            .chartScrollableAxes(.horizontal)

            Chart {
                ForEach(values, id: \.self) { value in
                    BarMark(
                        x: .value("X", value.x),
                        y: .value("Y", value.y),
                        width: .fixed(barWidth) // (25)
                    )
                    .foregroundStyle(colors[value.x % colors.count])
                }
            }
            .chartScrollableAxes(.horizontal)
            .chartXVisibleDomain(length: chartLength) // 12)
        }
        .padding()
        .onAppear() {
            var rnd = SystemRandomNumberGenerator()
            let numberOfValues = 50
            for index in 0 ..< numberOfValues {
                values.append(Value(x: index, y: Int(rnd.next() % 50)))
            }
            chartLength = Int(UIScreen.main.bounds.width / (barWidth+5))
        }
    }
}

In theory that does work maybe kind of in some cases. I did test it myself at some point and it did kind of work, but as soon as I add chartXVisibleDomain to my real app it freezes. If I remove that line it works ok but the chart is as buggy as ever. Any tested value causes it to freeze as soon as the chart should be shown (by default the chart that's buggy is in a second tab in a TabView). Freezes both in the simulator and on device.

By now I could have created my bar chart from scratch instead of fighting with this buggy mess.

Aha, when googling for freezes when using that modifier I found: https://stackoverflow.com/questions/77236097/swift-charts-chartxvisibledomain-hangs-or-crashes. The docs here are really bad, they had me believing that the value was the number of visible bars. Maybe someone should fix that and also add an example where the units really matter. But the whole thing should of course not freeze when the value is not ideal. In my case I have one bar for each hour and I show about 36 hours of data. The X axis comes from a Date. So setting a value of 10 here means that the visible area is 10 seconds. A better value 36000 which is 10 hours and which gives about 10 visible bars.

I tested the code you posted and it works. Value of length is 12, and 2 hours later, it still scrolls.

So, what is your point ? You speak of Date format fut

struct Value: Hashable {
    let x: Int
    let y: Int
}

is plain Int…

It seems you misread the documentation:

chartXVisibleDomain(length:)
Sets the length of the visible domain in the X dimension.

func chartXVisibleDomain<P>(length: P) -> some View where P : Plottable, P : Numeric
Parameters
length
The length of the visible domain measured in data units. For categorical data, this should be the number of visible categories.
Discussion
Use this method to control how much of the chart is visible in a scrollable chart. The example below sets the visible portion of the chart to 10 units in the X axis.

You say

In theory that does work maybe kind of in some cases. I did test it myself at some point and it did kind of work, but as soon as I add chartXVisibleDomain to my real app it freezes.

Does it work or not ? In which cases ?

May be the problem is with the TabView (but you did not share code)