SwiftUI Chart y-axis data does not align properly when using dates

120 Views Asked by At

I created a Line Chart in SwiftUI.

Drawing a Chart wasn't a problem. But I don't know why data doesn't align with y-axis label and grid line. The data is plotted slightly below.

enter image description here

It's just basic code. So I don't have anything to try some.

Is just SwiftUI Bug?

Does anyone know about this issue?

This is the whole code

import SwiftUI
import Charts

struct Time {
    let date: String
    let time: String
}

struct ChartsView: View {
    
    @State var timeData: [Time] = [
        .init(date: "2023-12-01", time: "07:50:00"),
        .init(date: "2023-12-02", time: "08:00:00"),
        .init(date: "2023-12-03", time: "08:10:00"),
        .init(date: "2023-12-04", time: "08:00:00"),
        .init(date: "2023-12-05", time: "08:00:00"),
        .init(date: "2023-12-06", time: "07:50:00"),
        .init(date: "2023-12-07", time: "08:00:00"),
        .init(date: "2023-12-08", time: "07:50:00"),
        .init(date: "2023-12-09", time: "08:10:00"),
        .init(date: "2023-12-10", time: "08:10:00"),
        .init(date: "2023-12-11", time: "08:10:00"),
        .init(date: "2023-12-12", time: "08:10:00"),
        .init(date: "2023-12-13", time: "07:50:00"),
        .init(date: "2023-12-14", time: "08:10:00"),
        .init(date: "2023-12-15", time: "07:50:00"),
        .init(date: "2023-12-16", time: "07:50:00"),
        .init(date: "2023-12-17", time: "08:00:00"),
        .init(date: "2023-12-18", time: "08:00:00"),
        .init(date: "2023-12-19", time: "08:00:00"),
        .init(date: "2023-12-20", time: "08:00:00"),
        .init(date: "2023-12-21", time: "08:00:00"),
        .init(date: "2023-12-22", time: "08:10:00"),
        .init(date: "2023-12-23", time: "08:00:00"),
        .init(date: "2023-12-24", time: "08:00:00"),
        .init(date: "2023-12-25", time: "08:00:00"),
        .init(date: "2023-12-26", time: "07:50:00"),
        .init(date: "2023-12-27", time: "08:00:00"),
        .init(date: "2023-12-28", time: "08:00:00"),
        .init(date: "2023-12-29", time: "08:00:00"),
        .init(date: "2023-12-30", time: "08:10:00"),
        .init(date: "2023-12-31", time: "07:50:00")
    ]
    
    var body: some View {
        ZStack(alignment: .bottom, content: {
            Chart {
                ForEach(timeData, id: \.date) {
                    LineMark(x: .value("x", $0.date.dateYYYYMMdd ?? Date(), unit: .day), 
                             y: .value("y", $0.time.dateHHmmss ?? Date(), unit: .minute))
                        .foregroundStyle(.red.opacity(0.3))
                        .lineStyle(StrokeStyle(lineWidth: 1))
                        .symbol {
                            Circle()
                                .fill(.red)
                                .frame(width: 4)
                        }
                    RuleMark(y: .value("y", "08:08:00".dateHHmmss ?? Date(), unit: .minute))
                }
            }
            .frame(height: 165)
            .padding(24)
        })        
    }
}

extension String {
    var dateYYYYMMdd: Date? {
        let formatter = DateFormatter()
        formatter.dateFormat = "YYYY-MM-dd"
        return formatter.date(from: self)
    }
    
    var dateHHmmss: Date? {
        let formatter = DateFormatter()
        formatter.dateFormat = "HH:mm:ss"
        return formatter.date(from: self)
    }
}
1

There are 1 best solutions below

0
Sweeper On BEST ANSWER

You should remove unit: .minute from the y axis' value.

The unit: plots the value in the half way point between two consecutive units that you specify. For example, unit: .minute would put 08:10 at 08:10:30, because that is the half way point between 08:10 and 08:11.

enter image description here

This is produced by 3 rule marks:

RuleMark(y: .value("y", "08:08:00".dateHHmmss ?? Date()))
    .foregroundStyle(.red)
RuleMark(y: .value("y", "08:08:00".dateHHmmss ?? Date(), unit: .minute))
    .foregroundStyle(.green)
RuleMark(y: .value("y", "08:09:00".dateHHmmss ?? Date()))
    .foregroundStyle(.blue)

While I can't think of a good reason to use unit: in line graphs, it is very useful when doing bar charts. BarMarks determine how wide the bar should be, then data is plotted in the centre point of the bar. If you combine this with axis labels centred between the grid lines of each unit, the axis labels would be at the bottom centres of the bars.

enter image description here

Code:

Chart {
    ForEach(data, id: \.date) {
        BarMark(x: .value("x", $0.date, unit: .day),
                y: .value("y", $0.y))
            .foregroundStyle(.red.opacity(0.3))
    }
}
.chartXAxis(content: {
    AxisMarks(values: AxisMarkValues.stride(by: .day)) { _ in
        AxisGridLine()
        AxisValueLabel(format: .dateTime.month().day(), centered: true)
    }
})