You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI Chart使用日期时Y轴数据与刻度网格不对齐问题求助

SwiftUI折线图数据点与Y轴标签/网格线不对齐(偏下移)

我在SwiftUI中用Charts框架创建了折线图,图表能正常绘制,但所有数据点都无法和Y轴的标签、网格线对齐,会略微偏下。

问题截图

目前只用基础代码实现,找不到排查方向,想知道这是SwiftUI的Bug吗?有没有人遇到过同样的问题?

完整代码如下:

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

问题原因与修复方案

这不是SwiftUI的Bug,核心问题出在时间字符串转Date时的默认日期偏差:

你的dateHHmmss扩展只转换时分秒,生成的Date会默认使用2000-01-01作为日期部分;而X轴的dateYYYYMMdd转换出来的是2023年12月的日期。Charts框架在计算Y轴位置时,会把整个Date对象(包含年月日时分秒)纳入计算,不同日期的同一天时间,因为时区、闰秒等因素,实际时间戳会有微小差异,导致数据点位置偏移。

另外,你在LineMark和RuleMark的y参数里加了unit: .minute,这个参数是用来截断日期单位而非仅提取时分秒,会进一步放大计算偏差。

修复步骤:

  1. 统一Date的基准日期:在转换时分秒时,把日期部分固定为和X轴一致的基准(比如用X轴对应的日期,或者统一用某一个固定日期)。

  2. 移除不必要的unit参数:如果只需要比较时分秒,不需要截断单位,直接传递转换后的Date即可。

修改后的代码示例:

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) { item in
                    guard let xDate = item.date.dateYYYYMMdd,
                          let yDate = item.time.dateHHmmss(with: xDate) else { return }
                    LineMark(x: .value("x", xDate), 
                             y: .value("y", yDate))
                        .foregroundStyle(.red.opacity(0.3))
                        .lineStyle(StrokeStyle(lineWidth: 1))
                        .symbol {
                            Circle()
                                .fill(.red)
                                .frame(width: 4)
                        }
                }
                RuleMark(y: .value("基准线", "08:08:00".dateHHmmss() ?? Date()))
                    .foregroundStyle(.gray.opacity(0.5))
            }
            .frame(height: 165)
            .padding(24)
        })
    }
}

extension String {
    var dateYYYYMMdd: Date? {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd" // 替换为小写yyyy,避免周年计算偏差
        return formatter.date(from: self)
    }
    
    func dateHHmmss(with baseDate: Date = Date(timeIntervalSince1970: 0)) -> Date? {
        let formatter = DateFormatter()
        formatter.dateFormat = "HH:mm:ss"
        guard let timeDate = formatter.date(from: self) else { return nil }
        // 提取时分秒,替换到基准日期上
        let calendar = Calendar.current
        let components = calendar.dateComponents([.hour, .minute, .second], from: timeDate)
        return calendar.date(bySettingHour: components.hour, minute: components.minute, second: components.second, of: baseDate)
    }
}

额外注意点:

  • 日期格式化时,YYYY是周年(基于周的年份),日常日期转换应该用yyyy(格里高利历年份),避免跨年时出现日期偏差。
  • RuleMark不需要放在ForEach里,否则会为每个数据点绘制一条重复的线,移到外面只绘制一次即可。

内容的提问来源于stack exchange,提问作者Justin.Shim

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 09:20:54