Search code examples
swiftswiftuiswiftui-charts

Swift Charts Customize xAxis Label Position?


I can't seem to find a way to control where the xAxis labels in a Swift Chart are positioned. By default this code results in 2 xAxis labels in the center of the chart (as pictured). I would like to customize this chart to show the date of the first sample on the far left and the date of the last sample on the far right. How can I accomplish this?

Chart {
      ForEach(buildChartRangedBarChartObjectsFrom(workoutStartDate: 
      workoutDetailViewModel.fullyLoadedWorkout?.startDate ?? Date(), workoutEndDate: 
      workoutDetailViewModel.fullyLoadedWorkout?.endDate ?? Date(), 
      customCyclingCadenceObjects: unwrappedCyclingCadenceObjects)) { 
      chartRangedBarChartObject in
          BarMark(
          x: .value("Date", chartRangedBarChartObject.periodStartDate),
          yStart: .value("Minimum Cadence During Minute", 
          chartRangedBarChartObject.minValue),
          yEnd: .value("Maximum Cadence During Minute", 
           chartRangedBarChartObject.maxValue),
           width: MarkDimension(integerLiteral: 5)
                                    )
          .foregroundStyle(Color.teal.gradient)
    }
}

enter image description here


Solution

  • First, since you are plotting data of each minute, you should add the unit: .minute parameter when creating your PlottableValue for the x axis. This makes it much easier for the system to automatically determine (aka "guess") which axis marks you want.

    You can add the axis value labels like this:

    .chartXAxis {
        AxisMarks(values: .automatic(desiredCount: 2)) { value in
            AxisValueLabel(anchor: value.index == 1 ? .topTrailing : .topLeading)
        }
    }
    

    Since you want a label on each side, the labels will have different alignments relative to the value they are associated with. The left label is "left-aligned" and the right label is "right-aligned".

    .automatic(desiredCount: 2) should always give you the values you want, but if you want to be extra sure, you can always just pass in an array of 2 Dates, extracted from your data.

    Here is a complete example, which passes the labels you want directly as an array

    struct Sample: Identifiable, Hashable {
        let start: Double
        let height: Double
        let time: Date
        
        var id: Date { time }
    }
    
    let data = (0..<30).map {
        Sample(start: .random(in: 10...20), height: .random(in: 10...16), time: Date().addingTimeInterval(Double($0) * 60))
    }
    
    struct ContentView: View {
        var body: some View {
            Chart(data) { sample in
                BarMark(
                    x: .value("Time", sample.time, unit: .minute),
                    yStart: .value("Min", sample.start),
                    yEnd: .value("Max", sample.start + sample.height),
                    width: 5
                )
            }
            .chartXAxis {
                AxisMarks(values: [data.first!.time, data.last!.time]) { value in
                    AxisValueLabel(
                        format: .dateTime.hour().minute(),
                        anchor: value.index == 1 ? .topTrailing : .topLeading
                    )
                }
            }
            .frame(width: 300, height: 200)
        }
    }
    

    Output:

    enter image description here