Search code examples
swiftswiftuichartsswiftui-charts

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


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)
    }
}

Solution

  • 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)
        }
    })