Search code examples
swiftcharts

How do I change the layering of different marks in Swift Charts


I have a Swift Chart. It has two sets of data plotted. A BarMark using yStart and yEnd to specify minimum/maximum values and a LineMark to specify a related value.

I want the LineMark plot to be "on top" of the BarMark plot, but it always plots the other way around, with the result that the bars obscure part of the line.

How can I change this?

Here is a simple reproduction of my problem

struct DataItem: Identifiable {
    var id: Int {
        return month
    }
    let month: Int
    let min: Int
    let max: Int
    let mid: Int
    
    static var data:[DataItem] = {
        
        var items = [DataItem]()
        
        for i in 1...12 {
            let min = Int.random(in: 1...50)
            let max = Int.random(in: 51...100)
            let mid = (max-min)/2+min
            items.append(DataItem(month: i, min: min, max: max, mid:mid))
            print(max,min,mid)
        }
        
        return items
        
    }()
}


import SwiftUI
import Charts

struct ChartView: View {
    var data: [DataItem]
    var body: some View {
        Chart(data) { dataItem in
            BarMark(x: .value("Month",  dataItem.month), yStart: .value("Min", dataItem.min), yEnd: .value("Max", dataItem.max))
            LineMark(x: .value("Month",dataItem.month), y:.value("mid", dataItem.mid))
                .symbol(.circle)
                .foregroundStyle(.green)
        }
    }
}

#Preview {
    ChartView(data: DataItem.data)
}

In the image, you can see that the first LineMark appears on top of the first BarMark but the rest are behind. Why is this and how can I change it?

enter image description here


Solution

  • It turns out the answer is a subtle but significant change to the way that the marks are added to the Chart.

    Using the form in my question

    Chart(data) {
        BarMark() 
        LineMark()
    }
    

    results in a chart with effectively a single plot with data represented in two different ways.

    If I change it to

    Chart { 
        ForEach(data) { 
            BarMark() 
        } 
        ForEach(data) {
            LineMark()
        }
    }
    

    Then I have one chart with two separate plots of two separate data series and the layer order follows the order in which those plots are specified.

    Here is the working code:

    struct ChartView: View {
        var data: [DataItem]
        var body: some View {
            Chart {
                ForEach(data) { dataItem in
                    BarMark(x: .value("Month",  dataItem.month), yStart: .value("Min", dataItem.min), yEnd: .value("Max", dataItem.max))
                }
                ForEach(data) { dataItem in
                    LineMark(x: .value("Month",dataItem.month), y:.value("mid", dataItem.mid))
                        .symbol(.circle)
                        .foregroundStyle(.green)
                }
            }
        }
    }
    

    Result: enter image description here