iosswiftuicharts

Swift UI PointMark Icons Cut Off on Y-Axis with Date Data in SwiftUI Char


The issue with displaying PointMarks in SwiftUI Chart is that when I use PointMark to display multiple point icons, and the Y-axis represents Date type data, the icons are not fully visible. The topmost point icon is cut off, with half of it not being displayed.

Running result

To solve this issue, I tried adding a range to the Y-axis by calculating the minimum and maximum values in the code using .chartYAxisRange(min: adjustedMinValue, max: adjustedMaxValue), but it keeps throwing an error: "The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions, I commented out that section of the code.

If anyone could help take a look at this issue, I would greatly appreciate it.

import Foundation
import SwiftUI
import Charts



func convertFullDateToHMDate(_ fullDateString: String) -> Date {
    let fullFormatter = DateFormatter()
    fullFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
    
   
    let fullDate = fullFormatter.date(from: fullDateString)!
    
    let calendar = Calendar.current
    

    let components = calendar.dateComponents([.hour, .minute ,.second], from: fullDate)
    

    return calendar.date(from: components)!
}


func formatDateToHMS(_ date: Date) -> String {
    let formatter = DateFormatter()
    formatter.dateFormat = "HH:mm:ss"
    return formatter.string(from: date)
}

struct TemperatureData: Identifiable ,Codable {
    var id = UUID()
    var day: String
    var detailDay: String // detail time
    var temperature: Double = 0.0
}




struct PointChartView: View {
    var lineChartViewData: LineChartViewData
    @State private var selectedWeek: Int = 0
    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
//            let (minDate, maxDate) = lineChartViewData.getMinMaxDate()
            TabView(selection: $selectedWeek) {
                ForEach(0..<lineChartViewData.getWeeklyData().count, id: \.self) { weekIndex in
                    VStack{
                        Chart (lineChartViewData.getWeeklyData()[weekIndex]) { item in
                            
                            PointMark(
                                x: .value("day", item.day),
                                y: .value("time", convertFullDateToHMDate(item.detailDay))
                            )
                            .symbol {
                                
                                Image(systemName: "square.fill")
                                    .resizable()
                                    .frame(width: 15, height: 15)
                                    .foregroundStyle(Color.green)
                            }
                            
                        }
                        .chartYAxis {
                            AxisMarks(position: .leading) { value in
                                if let dateValue = value.as(Date.self) {
                                    AxisTick()
                                    AxisGridLine()
                                    AxisValueLabel(formatDateToHMS(dateValue))
                                } else {
                                    AxisValueLabel("\(value.as(Double.self) ?? 0.0)")
                                }
                            }/* .chartYAxisRange(min: minDate, max: maxDate)*/
                
                            
                        }
                    }.tag(weekIndex)
                }
            }.frame(height: 150).tabViewStyle(PageTabViewStyle())
                .onAppear {
                  
                    selectedWeek = lineChartViewData.getWeeklyData().count - 1
                }

           
        }
        .padding(16)
        .background(
            ZStack {
                if lineChartViewData.isArtificial {
                    Rectangle()
                        .fill(Color.gray.opacity(0.5))
                        .cornerRadius(30)
                        .background(.ultraThinMaterial)
                } else {
                    Rectangle()
                        .fill(Color.gray.opacity(0))
                        .background(.ultraThinMaterial) // 直接设置背景材质
                        .cornerRadius(30)
                }
            }
        )
    }
}


struct LineChartViewData : Identifiable ,Codable {
    var id = UUID()
    var imageName: String
    var isArtificial: Bool
    var dataArray:  [TemperatureData]  = []

    
    func getWeeklyData()-> [[TemperatureData]] {
        var twoDimensionalArray: [[TemperatureData]] = []
        var subarray: [TemperatureData] = []
        for element in self.dataArray {
            subarray.append(element)
            if subarray.count == 7 {
                twoDimensionalArray.append(subarray)
                subarray = []
            }
        }
        if(!subarray.isEmpty) {
            twoDimensionalArray.append(subarray)
        }
        return twoDimensionalArray
    }
    
    func getMinMaxDate() -> (min: Date, max: Date) {
           let values = dataArray.map { convertFullDateToHMDate($0.detailDay) }
           let minValue = values.min() ?? Date()
           let maxValue = values.max() ?? Date()
           let padding: TimeInterval = 60 * 10
           let adjustedMinValue = minValue.addingTimeInterval(-padding)
           let adjustedMaxValue = maxValue.addingTimeInterval(padding)
           
           return (adjustedMinValue, adjustedMaxValue)
       }
}


struct PointChartView_Previews: PreviewProvider {
    static var previews: some View {
   
        let temperatureData: [TemperatureData] = [
            TemperatureData(day: "9-9", detailDay: "2024-12-01 10:31:00"),
            TemperatureData(day: "9-9", detailDay: "2024-12-01 10:32:00"),
            TemperatureData(day: "9-9", detailDay: "2024-12-01 10:33:00"),
        ]
        PointChartView(lineChartViewData:LineChartViewData( imageName: "IconPoop" ,isArtificial:false,dataArray: temperatureData))
        
        
    }
}


Solution

  • Here is my full test code that works well for me with .chartYScale(domain: ...) and calculating minDate and maxDate only once.

    Tested on real devices (not Previews), iOS-18 and MacCatalyst and MacOS 15.2 using XCode 16.2.

    struct PointChartView: View {
        var lineChartViewData: LineChartViewData
        @State private var selectedWeek: Int = 0
        
        @State private var drange: (min: Date, max: Date) = (Date(), Date()) // <--- here
    
        var body: some View {
            VStack(alignment: .leading, spacing: 16) {
                TabView(selection: $selectedWeek) {
                    ForEach(0..<lineChartViewData.getWeeklyData().count, id: \.self) { weekIndex in
                        VStack{
                            Chart (lineChartViewData.getWeeklyData()[weekIndex]) { item in
                                PointMark(
                                    x: .value("day", item.day),
                                    y: .value("time", convertFullDateToHMDate(item.detailDay))
                                )
                                .symbol {
                                    Image(systemName: "square.fill")
                                        .resizable()
                                        .frame(width: 15, height: 15)
                                        .foregroundStyle(Color.green)
                                }
                            }
                            .chartYAxis {
                                AxisMarks(position: .leading) { value in
                                    if let dateValue = value.as(Date.self) {
                                        AxisTick()
                                        AxisGridLine()
                                        AxisValueLabel(formatDateToHMS(dateValue))
                                    } else {
                                        AxisValueLabel("\(value.as(Double.self) ?? 0.0)")
                                    }
                                }
                            }
                            .chartYScale(domain: drange.min...drange.max) // <--- here
                        }.tag(weekIndex)
                    }
                }
                .frame(height: 150)
                .tabViewStyle(PageTabViewStyle()) // not on macOS
                .onAppear {
                    selectedWeek = lineChartViewData.getWeeklyData().count - 1
                    drange = lineChartViewData.getMinMaxDate() // <--- here
                }
            }
            .padding(16)
            .background(
                ZStack {
                    if lineChartViewData.isArtificial {
                        Rectangle()
                            .fill(Color.gray.opacity(0.5))
                            .cornerRadius(30)
                            .background(.ultraThinMaterial)
                    } else {
                        Rectangle()
                            .fill(Color.gray.opacity(0))
                            .background(.ultraThinMaterial) // 直接设置背景材质
                            .cornerRadius(30)
                    }
                }
            )
        }
    }
    
    struct LineChartViewData : Identifiable ,Codable {
        var id = UUID()
        var imageName: String
        var isArtificial: Bool
        var dataArray:  [TemperatureData]  = []
    
        func getWeeklyData()-> [[TemperatureData]] {
            var twoDimensionalArray: [[TemperatureData]] = []
            var subarray: [TemperatureData] = []
            for element in self.dataArray {
                subarray.append(element)
                if subarray.count == 7 {
                    twoDimensionalArray.append(subarray)
                    subarray = []
                }
            }
            if(!subarray.isEmpty) {
                twoDimensionalArray.append(subarray)
            }
            return twoDimensionalArray
        }
        
        func getMinMaxDate() -> (min: Date, max: Date) {
               let values = dataArray.map { convertFullDateToHMDate($0.detailDay) }
               let minValue = values.min() ?? Date()
               let maxValue = values.max() ?? Date()
               let padding: TimeInterval = 60 * 10
               let adjustedMinValue = minValue.addingTimeInterval(-padding)
               let adjustedMaxValue = maxValue.addingTimeInterval(padding)
    
               return (adjustedMinValue, adjustedMaxValue)
           }
    }
    
    struct PointChartView_Previews: PreviewProvider {
        static var previews: some View {
       
            let temperatureData: [TemperatureData] = [
                TemperatureData(day: "9-9", detailDay: "2024-12-01 10:31:00"),
                TemperatureData(day: "9-9", detailDay: "2024-12-01 10:32:00"),
                TemperatureData(day: "9-9", detailDay: "2024-12-01 10:33:00"),
            ]
            
            PointChartView(lineChartViewData: LineChartViewData(imageName: "IconPoop" ,isArtificial: false, dataArray: temperatureData))
        }
    }
    
    struct ContentView: View {
        let temperatureData: [TemperatureData] = [
            TemperatureData(day: "9-9", detailDay: "2024-12-01 10:31:00"),
            TemperatureData(day: "8-8", detailDay: "2024-12-02 10:32:00"),
            TemperatureData(day: "7-7", detailDay: "2024-12-03 10:33:00"),
        ]
        
        var body: some View {
            PointChartView(lineChartViewData: LineChartViewData(imageName: "IconPoop" ,isArtificial: false, dataArray: temperatureData))
        }
    }
    
    func convertFullDateToHMDate(_ fullDateString: String) -> Date {
        let fullFormatter = DateFormatter()
        fullFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
        let fullDate = fullFormatter.date(from: fullDateString)!
        let calendar = Calendar.current
        let components = calendar.dateComponents([.hour, .minute ,.second], from: fullDate)
        return calendar.date(from: components)!
    }
    
    func formatDateToHMS(_ date: Date) -> String {
        let formatter = DateFormatter()
        formatter.dateFormat = "HH:mm:ss"
        return formatter.string(from: date)
    }
    
    struct TemperatureData: Identifiable ,Codable {
        var id = UUID()
        var day: String
        var detailDay: String // detail time
        var temperature: Double = 0.0
    }