swiftuiswiftcharts

BarChart with two datasets: How to postion bar side by side instead of stacked?


I am trying to create a BarChart which shows two datasets side-by-side: The current values and the average values. No matter what I try, the bars are always stacked.

According to all information I found, this should be possible by using .foregroundStyle(by: .value(...)) and/or .position(by: .value(...)). However, this does not has effect here.

To start with, I would be happy to place the bars side-by-side. The final goal would be to place the average-bars slightly offset behind the current-bars.

import SwiftUI
import Charts

struct ContentView: View {
    let currentData: [DayData] = (1...15).map { DayData(day: $0, value: Double.random(in: 20...100)) }
    let averageData: [DayData] = (1...15).map { DayData(day: $0, value: Double.random(in: 50...70)) }
    
    var body: some View {
        Chart {
            ForEach(currentData) { day in
                BarMark(
                    x: .value("Day", day.day),
                    y: .value("Current", day.value)
                )
                .foregroundStyle(by: .value("Data Type", "Current"))
                .position(by: .value("Data Type", "Current"))
            }
            
            ForEach(averageData) { day in
                BarMark(
                    x: .value("Day", day.day),
                    y: .value("Average", day.value)
                )
                .foregroundStyle(by: .value("Data Type", "Average"))
                .position(by: .value("Data Type", "Average"))
            }
        }
        .frame(height: 300)
        .padding()
        .chartXAxis {
            AxisMarks(values: currentData.map { $0.day }) { _ in
                AxisTick()
                AxisGridLine()
                AxisValueLabel()
            }
        }
    }
}

// Datensatz für einen Tag
struct DayData: Identifiable {
    let id = UUID()
    let day: Int
    let value: Double
}

#Preview {
    ContentView()
}

enter image description here


Solution

  • This is because the X axis of your chart is numerical data, not categorical data (strings).

    If you write x: .value("Day", "\(day.day)") instead, the bars will be side-by-side as you expect. Since this is categorical data, you don't need to explicitly pass all the x labels to AxisMarks either.

    If you want to keep the data numerical, you need to specify a width: for the bars, and also pass the axis and span parameters of position(by:), because SwiftUI can't figure these out automatically for numerical data.

    ForEach(currentData) { day in
        BarMark(
            x: .value("Day", day.day),
         // this example works for categorical data too if you prefer that
         // x: .value("Day", "\(day.day)"),
            y: .value("Current", day.value),
            width: 12
        )
        .foregroundStyle(by: .value("Data Type", "Current"))
        .position(by: .value("Data Type", "Current"), axis: .horizontal, span: 20)
    }
    
    ForEach(averageData) { day in
        BarMark(
            x: .value("Day", day.day),
            y: .value("Average", day.value),
            width: 12
        )
        .foregroundStyle(by: .value("Data Type", "Average"))
        .position(by: .value("Data Type", "Average"), axis: .horizontal, span: 20)
    }
    .zIndex(-1)
    

    Here I have made the width of each bar slightly bigger than half of the span, and set the average bars to a lower z-index. I assume this is what you mean when you say you want the average bars to be "behind" the current bars.

    The values you pass to width and span don't need to be simple numbers. They are MarkDimensions. Read the documentation to learn more about what you can do with them.