Swift Charts: .hour annotations do not appear

I'm building a Swift Chart that displays locations in a horizontal timeline. The chart is scrollable.

When the chart is zoomed in, I want to show an annotation for every 6 hours.

Unfortunately, when axisMarks are set to .stride(by: .hour, count: 6), the annotations do not appear for the first several months in the timeline. I tried setting .stride(by: .minute, count: 360), but the result is the same.

Is this a Swift Charts bug, or am I doing something wrong?

A reproducible example is below. To reproduce:

  1. Run the code below
  2. See that annotations are missing at the leading edge of the chart. They only show up from a certain point on the chart's domain.

Tested on various iPhone and iPad simulators and physical devices, the issue appears everywhere.

P.S. I am aware that the example code below is not performant and that the annotations overlap when the chart is zoomed out. I have workarounds for that, but it's beyond the scope of my question and the minimum reproducible example.

struct ChartAnnotationsBug: View {

  /// Sample data
  let data = SampleData.samples
  let startDate = SampleData.samples.first?.startDate ?? Date()
  let endDate = Date()
  /// Scroll position of the chart, expressed as Date along the x-axis.
  @State var chartPosition: Date = SampleData.samples.first?.startDate ?? Date()
  /// Sets the granularity of the shown view.
  @State var visibleDomain: VisibleDomain = .month

  var body: some View {

    Chart(data, id: \.id) { element in
      BarMark(xStart: .value("Start", element.startDate),
              xEnd: .value("End", element.endDate),
              yStart: 0,
              yEnd: 50)
      .foregroundStyle(by: .value("Type", element.type.rawValue))
      .clipShape(.rect(cornerRadius: 8, style: .continuous))
    }
    .chartScrollableAxes(.horizontal) // enable scroll
    .chartScrollPosition(x: $chartPosition) // track scroll offset
    .chartXVisibleDomain(length: visibleDomain.seconds)
    .chartXScale(domain: startDate...endDate)
    .chartXAxis {
      AxisMarks(values: .stride(by: .hour, count: 6)) { value in
        if let date = value.as(Date.self) {
          let hour = Calendar.current.component(.hour, from: date)
          if hour == 0 { // midnight
            AxisValueLabel(collisionResolution: .truncate) {
              VStack(alignment: .leading) {
                Text(date, format: .dateTime.hour().minute())
                Text(date, format: .dateTime.weekday().month().day())
                  .bold()
              }
            }
            AxisTick(stroke: .init(lineWidth: 1))
          } else if [6, 12, 18].contains(hour) { // period
            AxisValueLabel(collisionResolution: .truncate) {
              Text(date, format: .dateTime.hour().minute())
            }
            AxisTick(length: .label)
          }
        }
      }
    }
    .frame(height: 100)
    .padding(.bottom, 40) // for overlay picker
    .overlay {
      Picker("", selection: $visibleDomain.animation()) {
        ForEach(VisibleDomain.allCases) { variant in
          Text(variant.label)
            .tag(variant)
        }
      }
      .pickerStyle(.segmented)
      .frame(width: 240)
      .padding(.trailing)
      .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomTrailing)
    } //: overlay
  } //: body
} //: struct

// MARK: - Preview
#Preview {
  ChartAnnotationsBug()
}

// MARK: - Data
enum SampleDataType: String, CaseIterable {
  case city, wood, field

  var label: String {
    switch self {
    case .city:
      "City"
    case .wood:
      "Wood"
    case .field:
      "Field"
    }
  }
}

enum VisibleDomain: Identifiable, CaseIterable {

  case day
  case week
  case month

  var id: Int {
    self.seconds
  }

  var seconds: Int {
    switch self {
    case .day:
      3600 * 24 * 2
    case .week:
      3600 * 24 * 10
    case .month:
      3600 * 24 * 40
    }
  }

  var label: String {
    switch self {
    case .day:
      "Days"
    case .week:
      "Weeks"
    case .month:
      "Months"
    }
  }
}

struct SampleData: Identifiable {
  let startDate: Date
  let endDate: Date
  let name: String
  let type: SampleDataType
  var id: String { name }

  static let samples: [SampleData] = [
    .init(startDate: Date.from(year: 2024, month: 3, day: 1, hour: 23, minute: 59),
          endDate: Date.from(year: 2024, month: 3, day: 10),
          name: "New York",
          type: .city),
    .init(startDate: Date.from(year: 2024, month: 3, day: 10, hour: 6),
          endDate: Date.from(year: 2024, month: 3, day: 20),
          name: "London",
          type: .city),
    .init(startDate: Date.from(year: 2024, month: 3, day: 20),
          endDate: Date.from(year: 2024, month: 4, day: 10),
          name: "Backcountry ABC",
          type: .field),
    .init(startDate: Date.from(year: 2024, month: 4, day: 10),
          endDate: Date.from(year: 2024, month: 4, day: 20),
          name: "Field DEF",
          type: .field),
    .init(startDate: Date.from(year: 2024, month: 4, day: 20),
          endDate: Date.from(year: 2024, month: 5, day: 10),
          name: "Wood 123",
          type: .wood),
    .init(startDate: Date.from(year: 2024, month: 5, day: 10),
          endDate: Date.from(year: 2024, month: 5, day: 20),
          name: "Paris",
          type: .city),
    .init(startDate: Date.from(year: 2024, month: 5, day: 20),
          endDate: Date.from(year: 2024, month: 6, day: 5),
          name: "Field GHI",
          type: .field),
    .init(startDate: Date.from(year: 2024, month: 6, day: 5),
          endDate: Date.from(year: 2024, month: 6, day: 10),
          name: "Wood 456",
          type: .wood),
    .init(startDate: Date.from(year: 2024, month: 6, day: 10),
          endDate: Date(),
          name: "Field JKL",
          type: .field)
  ]
}

extension Date {
  /**
   Constructs a Date from a given year (Int). Use like `Date.from(year: 2020)`.
   */
  static func from(year: Int? = nil, month: Int? = nil, day: Int? = nil, hour: Int? = nil, minute: Int? = nil) -> Date {
    let components = DateComponents(year: year, month: month, day: day, hour: hour, minute: minute)
    guard let date = Calendar.current.date(from: components) else {
      print(#function, "Failed to construct date. Returning current date.")
      return Date()
    }
    return date
  }
}
Swift Charts: .hour annotations do not appear
 
 
Q