iosswiftuicharts

Swift ui Chart Issue: Date Type on Y-Axis Showing Smaller Time on Top and Larger Time on Bottom in BarMark


There are totaly 2 questions

  1. When using SwiftUI's Chart with BarMark for plotting data with Date values on the Y-axis, the issue arises where the time displayed on the Y-axis appears to be inverted—i.e., the smaller times (earlier hours) are shown at the top, while the larger times (later hours) are displayed at the bottom.

  2. There is another question, I don't know why this code always reports The compiler is unable to type check this expression in reasonable time ; try breaking up the expression into distinct sub-expressions

The second issue is caused by extracting this section of the code for display from the original code.

import SwiftUI
import Charts

// Helper function to extract the hour and minute from a Date
func extractHourMinuteFromDate(_ date: Date) -> Date {
    let calendar = Calendar.current
    let components = calendar.dateComponents([.hour, .minute], from: date)
    return calendar.date(from: components) ?? date
}



// Helper function to convert a time string to TimeInterval (if needed)
func timeStringToTimeInterval(_ timeString: String) -> TimeInterval? {
    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = "HH:mm:ss"
    if let date = dateFormatter.date(from: timeString) {
        return date.timeIntervalSinceReferenceDate
    }
    return nil
}

struct HomeHistogramView: View {
    var viewData: HistogramViewData
    @State private var selectedWeek: Int = 0
    
    var body: some View {

        let weeklyData=viewData.getWeeklyData()
        TabView(selection: $selectedWeek) {
            ForEach(0..<weeklyData.count, id: \.self) { weekIndex in
                VStack {
                    Chart(weeklyData[weekIndex]) { item in
                        BarMark(
                            x: .value("Date", item.date),
                            yStart: .value("Start", item.start),
                            yEnd: .value("End", item.end),
                            width: 30
                        )
                        .foregroundStyle(Color(viewData.chartColor))
                        .cornerRadius(4)
                    }
                    .chartYAxis {
                        AxisMarks(position: .leading) {
                            AxisGridLine()
                            AxisTick()
                            AxisValueLabel(format: .dateTime.hour(.twoDigits(amPM: .omitted)).minute(.twoDigits))
                        }
                    }
                    .frame(height: 130)
                    
                }
                .tag(weekIndex)
                
            }
            .tabViewStyle(PageTabViewStyle())
            .frame(height: 150)
            .onAppear {
                // Default to the last week's data
                selectedWeek = weeklyData.count - 1
            }
        }
        .padding(16)
    }
}

// Function to create a Date object from hour and minute
func dateFrom(hour: Int, minute: Int) -> Date {
    let calendar = Calendar.current
    var components = DateComponents()
    components.hour = hour
    components.minute = minute
    return calendar.date(from: components) ?? Date()
}

struct myData: Identifiable, Codable {
    var id = UUID()
    var date: String
    var detailDate: String
    var start: Date
    var end: Date
}

struct HistogramViewData: Identifiable, Codable {
    var id = UUID()
    var time: String
    var isArtificial: Bool
    var dataArray: [myData] = []
    
    // Method to get weekly data
    func getWeeklyData() -> [[myData]] {
        var twoDimensionalArray: [[myData]] = []
        var subarray: [myData] = []
        
        for element in self.dataArray {
            subarray.append(element)
            if subarray.count == 7 {
                twoDimensionalArray.append(subarray)
                subarray = []
            }
        }
        
        if !subarray.isEmpty {
            twoDimensionalArray.append(subarray)
        }
        
        return twoDimensionalArray
    }
}

struct MainChartsView_Previews: PreviewProvider {
    static var previews: some View {
        let sampleSleepData: [myData] = [
            myData(date: "9", detailDate: "9", start: extractHourMinuteFromDate(dateFrom(hour: 6, minute: 0)), end: extractHourMinuteFromDate(dateFrom(hour: 22, minute: 0))),
            myData(date: "10", detailDate: "9", start: extractHourMinuteFromDate(dateFrom(hour: 6, minute: 0)), end: extractHourMinuteFromDate(dateFrom(hour: 23, minute: 0))),
            myData(date: "11", detailDate: "9", start: extractHourMinuteFromDate(dateFrom(hour: 6, minute: 30)), end: extractHourMinuteFromDate(dateFrom(hour: 21, minute: 30))),
            myData(date: "12", detailDate: "9", start: extractHourMinuteFromDate(dateFrom(hour: 0, minute: 0)), end: extractHourMinuteFromDate(dateFrom(hour: 8, minute: 0))),
            myData(date: "13", detailDate: "9", start: extractHourMinuteFromDate(dateFrom(hour: 6, minute: 30)), end: extractHourMinuteFromDate(dateFrom(hour: 7, minute: 30)))
        ]
        
        let viewData = HistogramViewData(
            time: "8:42 ",
            isArtificial: true,
            dataArray: sampleSleepData
        )
        
        HomeHistogramView(viewData: viewData)
    }
}

enter image description here

I have explored different approaches but none of them have successfully addressed the problem. I need a solution that ensures the time values are displayed correctly, with earlier times at the bottom and later times at the top, as expected.

It would be greatly appreciated if someone could explain this.


Solution

  • The default behaviour for displaying dates on the Y axis is to show earlier dates on the top. This makes sense because the natural reading direction is from top to bottom. If your graph is showing a time series of events, it is natural for people to start at the top and read downwards.

    You can manually reverse the axis by using

    .chartYScale(domain: .automatic(reversed: true))