Search code examples
swiftswiftuiswiftui-charts

Why AreaMark doesn't work where BarMark and PointMark works perfectly?


I'm trying to visualize some data using AreaMark of Swift Charts, however, different data structures seem to affect the result. Really confused here.

The following two example structs hold the same data, but in different ways:

import SwiftUI
import Charts

struct Food: Identifiable {
  let name: String
  let sales: Int
  let day: Int
  let id = UUID()
  
  
  init(name: String, sales: Int, day: Int) {
    self.name = name
    self.sales = sales
    self.day = day
  }
}

struct Sales: Identifiable {
  let day: Int
  let burger: Int
  let salad: Int
  let steak: Int
  let id = UUID()
  
  var total: Int {
    burger + salad + steak
  }
  
  init(day: Int, burger: Int, salad: Int, steak: Int) {
    self.day = day
    self.burger = burger
    self.salad = salad
    self.steak = steak
  }
}

Now if I populate data and use Swift Charts to plot the data, the first struct works perfectly with all types of charts. However, the second struct, while works well with BarMark and PointMark, doesn't seem to work with AreaMark.

[BarMark](https://i.sstatic.net/mVvad.png)
[PointMark](https://i.sstatic.net/IekH2.png)
[AreaMark](https://i.sstatic.net/AeL4Q.png)

To reproduce, change "AreaMark" to "BarMark" or "PointMark" in the following code:

struct ExperimentView: View {
  let cheeseburgerSalesByItem: [Food] = [
    .init(name: "Burger", sales: 29, day: 1),
    .init(name: "Salad", sales: 35, day: 1),
    .init(name: "Steak", sales: 30, day: 1),
    .init(name: "Burger", sales: 32, day: 2),
    .init(name: "Salad", sales: 38, day: 2),
    .init(name: "Steak", sales: 42, day: 2),
    .init(name: "Burger", sales: 35, day: 3),
    .init(name: "Salad", sales: 29, day: 3),
    .init(name: "Steak", sales: 41, day: 3),
    .init(name: "Burger", sales: 29, day: 4),
    .init(name: "Salad", sales: 38, day: 4),
    .init(name: "Steak", sales: 39, day: 4),
    .init(name: "Burger", sales: 43, day: 5),
    .init(name: "Salad", sales: 42, day: 5),
    .init(name: "Steak", sales: 30, day: 5),
    .init(name: "Burger", sales: 45, day: 6),
    .init(name: "Salad", sales: 39, day: 6),
    .init(name: "Steak", sales: 31, day: 6),
    .init(name: "Burger", sales: 37, day: 7),
    .init(name: "Salad", sales: 35, day: 7),
    .init(name: "Steak", sales: 30, day: 7),
  ]
  
  let cheeseburgerSalesByDay: [Sales] = [
    .init(day: 1, burger: 29, salad: 35, steak: 30),
    .init(day: 2, burger: 32, salad: 38, steak: 42),
    .init(day: 3, burger: 35, salad: 29, steak: 41),
    .init(day: 4, burger: 29, salad: 38, steak: 39),
    .init(day: 5, burger: 43, salad: 42, steak: 30),
    .init(day: 6, burger: 45, salad: 39, steak: 31),
    .init(day: 7, burger: 37, salad: 35, steak: 30)
  ]
  
  var body: some View {
    
    VStack {
      Chart(cheeseburgerSalesByItem) { sale in
        AreaMark(
          x: .value("Day", sale.day),
          y: .value("Sales", sale.sales)
        )
        .foregroundStyle(by: .value("Food Item", sale.name))
      }
      .chartXScale(domain: 1...7)
      .padding()
      
      Spacer()
      
      Chart(cheeseburgerSalesByDay) { sale in
        AreaMark(
          x: .value("Day", sale.day),
          y: .value("Burger", sale.burger)
        )
        .foregroundStyle(.brown)
        
                AreaMark(
                  x: .value("Day", sale.day),
                  y: .value("Salad", sale.salad)
                )
                .foregroundStyle(.green)
        
                AreaMark(
                  x: .value("Day", sale.day),
                  y: .value("Steak", sale.steak)
                )
                .foregroundStyle(.red)
      }
      .padding()
    }
    
    
  }
}

So what is the problem here? What to do if I want to use the second structure and plot a AreaMark chart?

If two of the three AreaMarks are commented out, the left one would work. Just more than one AreaMark plots don't work for this struct.


Solution

  • In Swift's Chart, each AreaMark(x:, y:) is interpreted as a distinct point.(informal). The chart connects these points to form lines and fill areas.

    In the first chart example you provided:

    Chart(cheeseburgerSalesByItem) { sale in
      AreaMark(
        x: .value("Day", sale.day),
        y: .value("Sales", sale.sales)
      )
      .foregroundStyle(by: .value("Food Item", sale.name))
    }
    .chartXScale(domain: 1...7)
    .padding()
    

    By applying .foregroundStyle(by: .value("Food Item", sale.name)), you are implicitly informing the Swift chart that the points belong to different lines (or areas), as they are categorized by the 'sale.name' value.

    However, in the second chart, you have not specified that the points (AxisMarks(x:, y:)) should be treated as separate lines(areas).

    You have two options:

    Use AreaMark(x:, y:, series:) to explicitly define separate series for your data points:

    Chart(cheeseburgerSalesByDay) { sale in
        AreaMark(
        x: .value("Day", sale.day),
        y: .value("Burger", sale.burger),
        series: .value("", 0)
      )
      .foregroundStyle(.brown)
      
      
      AreaMark(
        x: .value("Day", sale.day),
        y: .value("Salad", sale.salad),
        series: .value("", 1)
      )
      .foregroundStyle(.green)
      AreaMark(
        x: .value("Day", sale.day),
        y: .value("Steak", sale.steak),
        series: .value("", 2)
      )
      .foregroundStyle(.red)
    }
    .padding()
    

    Use .foregroundStyle(by:) to distinguish points by series without explicitly defining them in AreaMark:

    Chart(cheeseburgerSalesByDay) { sale 
        AreaMark(
        x: .value("Day", sale.day),
        y: .value("Burger", sale.burger)
      )
        .foregroundStyle(by: .value("", 0
      
      
      AreaMark(
        x: .value("Day", sale.day),
        y: .value("Salad", sale.salad)
      )
      .foregroundStyle(by: .value("", 1))
      AreaMark(
        x: .value("Day", sale.day),
        y: .value("Steak", sale.steak)
      )
      .foregroundStyle(by: .value("", 2))
    }
    .padding()
    

    I recommend the first solution because it allows you to explicitly control the color used for each series.