Reading time: 2 min

This recipe shows a more advanced SwiftUI bar chart that shows min, max and average for each data point. The end result looks like this:

Screenshot%202022-06-21%20at%2008.36.14

Charts framework is available starting in SwiftUI 4 (iOS 16, macOS 12.4).

Let's start off by creating a simple structure that represent air temperature in the city of Osijek, tracking minimal, maximal and mean values for each month. This is the data we'll be plotting in a bar chart.

struct Temperature {
  let month: Int
  let min: Int
  let max: Int
  let mean: Float
}

let data: [Temperature] = [
  Temperature(month: 1, min: -3, max: 4, mean: 0.8),
  Temperature(month: 2, min: -2, max: 7, mean: 2.4),
  Temperature(month: 3, min: 2, max: 13, mean: 7.2),
  Temperature(month: 4, min: 6, max: 18, mean: 12.3),
  Temperature(month: 5, min: 11, max: 23, mean: 17.1),
  Temperature(month: 6, min: 14, max: 27, mean: 20.5),
  Temperature(month: 7, min: 16, max: 29, mean: 22.2),
  Temperature(month: 8, min: 15, max: 29, mean: 22.1),
  Temperature(month: 9, min: 11, max: 23, mean: 17.3),
  Temperature(month: 10, min: 7, max: 18, mean: 12.5),
  Temperature(month: 11, min: 3, max: 11, mean: 6.9),
  Temperature(month: 12, min: -1, max: 5, mean: 1.6),
]

Instead of regular BarMark(x:y:), you can use BarMark(x:yStart:yEnd) to define the height of the bar relative to chart's Y axis. Then, use RectangleMark to draw a line on top of each bar to represent the average temperature for that month:

Chart {
  ForEach(data, id: \.month) { temperature in
    let month = Calendar.current.date(from: DateComponents(month: temperature.month))!
    BarMark(x: .value("Month", month, unit: .month),
            yStart: .value("Temperature", temperature.min),
            yEnd: .value("Temperature", temperature.max),
            width: .ratio(0.6))
      .opacity(0.3)

    RectangleMark(x: .value("Month", month, unit: .month),
                  y: .value("Temperature", temperature.mean),
                  width: .ratio(0.6),
                  height: .fixed(2))
  }
}

To draw a line indicating yearly average temperature, add a RuleMark with annotation:

Chart {
  // ... same as before ...

  let average = data.map(\.mean).reduce(0.0, +) / Float(data.count)
  RuleMark(y: .value("Mean", average))
    .foregroundStyle(.red)
    .lineStyle(StrokeStyle(lineWidth: 2))
    .annotation(position: .bottom, alignment: .trailing) {
      Text("Mean: \(average, format: .number.precision(.fractionLength(1)))")
        .font(.body.bold())
        .foregroundStyle(.red)
    }
}

Finally, you can modify the Y axis labels, just like we did in this recipe:

Chart {
  // ... same as before ...
}
.chartXAxis {
  AxisMarks(values: .stride(by: .month)) { _ in
    AxisGridLine()
    AxisTick()
    AxisValueLabel(format: .dateTime.month(.abbreviated), centered: true)
  }
}
.chartYAxis {
  AxisMarks(values: .automatic(desiredCount: 4)) { value in
    AxisGridLine()
    AxisTick()
    AxisValueLabel {
      if let plottable = value.as(Int.self) {
        Text("\(plottable) °C")
      }
    }
  }
}

Previous Post