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