Charts: How do I position x axis value labels below the associated grid lines

I am working with the new Charts Framework. The axis value labels for the x axis are dates (mm/dd/yy). I have 5 vertical grid lines and am trying to get the dates to be centered below the associated grid line with no success. One additional problem is that the values are in an array that contains 5 dates but only the first 4 get displayed. Any solutions will be appreciated. Below is the code.

struct LineChart: View {
    private var localArray: [TradingDayPrices]
    init(passedInArray: [TradingDayPrices]) {
        self.localArray = passedInArray
    }
    var body: some View {
        let minCloseValue: Double = localArray.map { $0.close }.min()!
        let maxCloseValue: Double  = localArray.map { $0.close }.max()!
        let minDate: Date = localArray[0].timeStamp!
        let itemCount: Int = localArray.count - 1
        let maxDate: Date = localArray[itemCount].timeStamp!
        Spacer()
        GroupBox (label: Text("VOO Closing Values For The Past Year")
            .font(.system(size: 20))
            .fontWeight(.bold)
            .frame(width: 700, height: 50, alignment: .center)) {
            Chart {
                ForEach(localArray) { item in
                    LineMark (
                        x: .value("TimeStamp", item.timeStamp!),
                        y: .value("Close", item.close)
                    )
                    .foregroundStyle(Color.blue)
                    .lineStyle(StrokeStyle(lineWidth: 1.25))
                } // end for each
            } // end chart
            .padding(50)
            .chartBackground { item in
                Color.white
            }
            .chartXAxisLabel(position: .bottom, alignment: .center) {
                Text("Date")
                    .font(.system(size: 14))
                    .foregroundColor(Color.black)
                    .frame(width: 50, height: 35, alignment: .bottom)
            }
            .chartYAxisLabel(position: .leading, alignment: .center, spacing: 0) {
                Text("Closing Values")
                    .font(.system(size: 14))
                    .foregroundColor(Color.black)
            }
            .chartXAxis {
                AxisMarks (values: GetXAxisLabels(min: minDate, max: maxDate)) { value in
                    AxisGridLine(stroke: StrokeStyle(lineWidth: 0.5))
                        .foregroundStyle(Color.gray)
                    AxisValueLabel() {
                        if let localDate = value.as(Date.self) {
                            let formattedDate = dateToStringFormatter.string(from: localDate)
                            Text(formattedDate)
                                .font(.system(size: 12))
                                .foregroundColor(Color.black)
                        }
                    } // end axis value label
                } // end axis marks
            } // end chart x axis
            .chartYAxis {
                AxisMarks (position: .leading, values: GetYAxisLabels(min: minCloseValue, max: maxCloseValue)) { value in
                    AxisGridLine(stroke: StrokeStyle(lineWidth: 0.5))
                        .foregroundStyle(Color.gray)
                    AxisValueLabel() {
                        if let localValue = value.as(Double.self) {
                            let formattedValue = String(format: "$%.2f", localValue)
                            Text(formattedValue)
                                .font(.system(size: 12))
                                .foregroundColor(Color.black)
                        }
                    }
                } // end axis marks
            } // end chart y axis
            .chartXScale(domain: minDate...maxDate)
            .chartYScale(domain: minCloseValue...maxCloseValue)
            .frame(width: 700, height: 700, alignment: .center)
            
        } // end group box
    } // end of body
} // end of structure
func GetXAxisLabels(min: Date, max: Date) -> [Date] {
    
    var xLabels: [Date] = []
    let increment: Int = 90
    for i in 0...3 {
        let value = Calendar.current.date(byAdding: DateComponents(day: (i * increment)), to: min)!
        xLabels.append(value)
    }
    xLabels.append(max)
    print("\(xLabels)")
    return xLabels
}
func GetYAxisLabels(min: Double, max: Double) -> [Double] {
    
    var yLabels: [Double] = []
    let increment: Double = (max - min) / 4
    for i in 0...3 {
        let value = min + (Double(i) * increment)
        yLabels.append(value)
    }
    yLabels.append(max)
    return yLabels
}
let dateToStringFormatter: DateFormatter = {
    let result = DateFormatter()
    result.dateFormat = "MM/dd/yy"
    return result
}()
Answered by ChrisMH in 737044022

After some additional research I was able to position values with the AxisValueLabel horizontal and vertical spacing parameters. I was not able to figure out how to get Charts to place a value under the far right vertical grid line, i.e. my 5th date. So I resolved the problem by removing the AxisValueLabel code and using an overlay along with x & y positioning of the values. See updated code below.

struct LineChart: View {
    
    private var localArray: [TradingDayPrices]
    private var chartTitle: String
    private var numYears: Int
    
    init(passedInArray: [TradingDayPrices], chartTitle: String, numYears: Int) {
        self.localArray = passedInArray
        self.chartTitle = chartTitle
        self.numYears = numYears
    }
    var body: some View {
        
        let minCloseValue: Double = localArray.map { $0.close }.min()!
        let maxCloseValue: Double  = localArray.map { $0.close }.max()!
        let minDate: Date = localArray[0].timeStamp!
        let itemCount: Int = localArray.count - 1
        let maxDate: Date = localArray[itemCount].timeStamp!
        let dateArray: [Date] = GetXAxisLabels(min: minDate, max: maxDate, numYears: numYears)
        
        Spacer()
        GroupBox (label: Text("\(chartTitle)")
            .font(Font.custom("Arial", size: 20))
            .frame(width: 700, height: 50, alignment: .center)) {
                
                Chart {
                    ForEach(localArray) { item in
                        LineMark (
                            x: .value("TimeStamp", item.timeStamp!),
                            y: .value("Close", item.close)
                        )
                        .foregroundStyle(Color.blue)
                        .lineStyle(StrokeStyle(lineWidth: 1.25))
                    } // end for each
                } // end chart
                .padding(50)
                .chartBackground { item in
                    Color.white
                }
                .chartXAxisLabel(position: .bottom, alignment: .center, spacing: 25) {
                    Text("")
                }
                .chartYAxisLabel(position: .leading, alignment: .center, spacing: 25) {
                    Text("Closing Value")
                        .font(.system(size: 14))
                        .foregroundColor(Color.black)
                }
                .chartXAxis {
                    AxisMarks (values: dateArray) { value in
                        AxisGridLine(stroke: StrokeStyle(lineWidth: 0.5))
                            .foregroundStyle(Color.gray)
                        AxisTick(centered: true, length: 7 , stroke: StrokeStyle(lineWidth: 0.5))
                            .foregroundStyle(Color.gray)
                    } // end axis marks
                } // end chart x axis
                .chartYAxis {
                    AxisMarks (position: .leading, values: GetYAxisLabels(min: minCloseValue, max: maxCloseValue)) { value in
                        AxisGridLine(stroke: StrokeStyle(lineWidth: 0.5))
                            .foregroundStyle(Color.gray)
                        
                        AxisTick(centered: true, length: 7 , stroke: StrokeStyle(lineWidth: 0.5))
                            .foregroundStyle(Color.gray)
                        
                        AxisValueLabel(verticalSpacing: 10) {
                            if let localValue = value.as(Double.self) {
                                let formattedValue = String(format: "$%.2f", localValue)
                                Text(formattedValue)
                                    .font(.system(size: 12))
                                    .foregroundColor(Color.black)
                            }
                        }
                    } // end axis marks
                } // end chart y axis
                .chartXScale(domain: minDate...maxDate)
                .chartYScale(domain: minCloseValue...maxCloseValue)
                .frame(width: 700, height: 700, alignment: .center)
                .overlay {
                    let dateValue0 = dateToStringFormatter.string(from: dateArray[0])
                    Text(dateValue0).position(x: 155, y: 620)
                    let dateValue1 = dateToStringFormatter.string(from: dateArray[1])
                    Text(dateValue1).position(x: 275, y: 620)
                    let dateValue2 = dateToStringFormatter.string(from: dateArray[2])
                    Text(dateValue2).position(x: 398, y: 620)
                    let dateValue3 = dateToStringFormatter.string(from: dateArray[3])
                    Text(dateValue3).position(x: 522, y: 620)
                    let dateValue4 = dateToStringFormatter.string(from: dateArray[4])
                    Text(dateValue4).position(x: 654, y: 620)
                    Text("Date").position(x: 398, y: 655)
                        .font(.system(size: 14))
                        .foregroundColor(Color.black)
                } // end overlay
            } // end group box frame
        Spacer().frame(height:65)
    } // end of body
} // end of structure
Accepted Answer

After some additional research I was able to position values with the AxisValueLabel horizontal and vertical spacing parameters. I was not able to figure out how to get Charts to place a value under the far right vertical grid line, i.e. my 5th date. So I resolved the problem by removing the AxisValueLabel code and using an overlay along with x & y positioning of the values. See updated code below.

struct LineChart: View {
    
    private var localArray: [TradingDayPrices]
    private var chartTitle: String
    private var numYears: Int
    
    init(passedInArray: [TradingDayPrices], chartTitle: String, numYears: Int) {
        self.localArray = passedInArray
        self.chartTitle = chartTitle
        self.numYears = numYears
    }
    var body: some View {
        
        let minCloseValue: Double = localArray.map { $0.close }.min()!
        let maxCloseValue: Double  = localArray.map { $0.close }.max()!
        let minDate: Date = localArray[0].timeStamp!
        let itemCount: Int = localArray.count - 1
        let maxDate: Date = localArray[itemCount].timeStamp!
        let dateArray: [Date] = GetXAxisLabels(min: minDate, max: maxDate, numYears: numYears)
        
        Spacer()
        GroupBox (label: Text("\(chartTitle)")
            .font(Font.custom("Arial", size: 20))
            .frame(width: 700, height: 50, alignment: .center)) {
                
                Chart {
                    ForEach(localArray) { item in
                        LineMark (
                            x: .value("TimeStamp", item.timeStamp!),
                            y: .value("Close", item.close)
                        )
                        .foregroundStyle(Color.blue)
                        .lineStyle(StrokeStyle(lineWidth: 1.25))
                    } // end for each
                } // end chart
                .padding(50)
                .chartBackground { item in
                    Color.white
                }
                .chartXAxisLabel(position: .bottom, alignment: .center, spacing: 25) {
                    Text("")
                }
                .chartYAxisLabel(position: .leading, alignment: .center, spacing: 25) {
                    Text("Closing Value")
                        .font(.system(size: 14))
                        .foregroundColor(Color.black)
                }
                .chartXAxis {
                    AxisMarks (values: dateArray) { value in
                        AxisGridLine(stroke: StrokeStyle(lineWidth: 0.5))
                            .foregroundStyle(Color.gray)
                        AxisTick(centered: true, length: 7 , stroke: StrokeStyle(lineWidth: 0.5))
                            .foregroundStyle(Color.gray)
                    } // end axis marks
                } // end chart x axis
                .chartYAxis {
                    AxisMarks (position: .leading, values: GetYAxisLabels(min: minCloseValue, max: maxCloseValue)) { value in
                        AxisGridLine(stroke: StrokeStyle(lineWidth: 0.5))
                            .foregroundStyle(Color.gray)
                        
                        AxisTick(centered: true, length: 7 , stroke: StrokeStyle(lineWidth: 0.5))
                            .foregroundStyle(Color.gray)
                        
                        AxisValueLabel(verticalSpacing: 10) {
                            if let localValue = value.as(Double.self) {
                                let formattedValue = String(format: "$%.2f", localValue)
                                Text(formattedValue)
                                    .font(.system(size: 12))
                                    .foregroundColor(Color.black)
                            }
                        }
                    } // end axis marks
                } // end chart y axis
                .chartXScale(domain: minDate...maxDate)
                .chartYScale(domain: minCloseValue...maxCloseValue)
                .frame(width: 700, height: 700, alignment: .center)
                .overlay {
                    let dateValue0 = dateToStringFormatter.string(from: dateArray[0])
                    Text(dateValue0).position(x: 155, y: 620)
                    let dateValue1 = dateToStringFormatter.string(from: dateArray[1])
                    Text(dateValue1).position(x: 275, y: 620)
                    let dateValue2 = dateToStringFormatter.string(from: dateArray[2])
                    Text(dateValue2).position(x: 398, y: 620)
                    let dateValue3 = dateToStringFormatter.string(from: dateArray[3])
                    Text(dateValue3).position(x: 522, y: 620)
                    let dateValue4 = dateToStringFormatter.string(from: dateArray[4])
                    Text(dateValue4).position(x: 654, y: 620)
                    Text("Date").position(x: 398, y: 655)
                        .font(.system(size: 14))
                        .foregroundColor(Color.black)
                } // end overlay
            } // end group box frame
        Spacer().frame(height:65)
    } // end of body
} // end of structure

I was struggling with this as well. I was able to solve this by setting AxisValueLabel's anchor to top.

            .chartXAxis {
                AxisMarks(position: .bottom, values: .stride(by: .hour, count: 3)) { _ in
                    AxisGridLine()
                    AxisValueLabel(format: .dateTime.hour(), anchor: .top)
                }
            }
Charts: How do I position x axis value labels below the associated grid lines
 
 
Q