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

Swift Charts X轴样式优化:刻度标签间距及首尾主刻度设置问题

Swift Charts 折线轴样式问题解决方案

需求

使用Swift Charts创建股票时间价值折线图,需实现:

  • 每月显示内置(inset)刻度
  • 起始、末尾及中间的主刻度尺寸更大,并在下方添加日期标签

现存问题

  1. 底部轴网格线与值标签间距异常:设置verticalSpacing: 0后,网格线与标签仍有约16pt间距,使用负数值虽能临时解决,但非合理方案。
  2. X轴首尾主刻度无法强制显示:当前.automatic(desiredCount:)生成的主刻度无法确保首尾日期必带标签。

解决方案

问题1:修复标签与刻度间距

额外间距源于你将主刻度和标签拆分为两组AxisMarks,各自占用独立布局空间。正确做法是将刻度与标签放在**同一组AxisMarks**中:

// 替换原X轴的主刻度+标签代码
AxisMarks(preset: .inset, values: .automatic(desiredCount: numberOfMajorTicks)) { value in
    AxisTick(length: 16, stroke: StrokeStyle(lineWidth: 1))
        .foregroundStyle(Color.black)
    AxisValueLabel(format: .dateTime.month().year(.twoDigits), verticalSpacing: 0)
}

此时verticalSpacing: 0会直接作用于刻度与标签的间距,无需负数值调整。

问题2:强制首尾日期作为主刻度

.automatic(desiredCount:)无法保证首尾必选,需手动组合首尾日期与中间自动刻度:

.chartXAxis {
    let numberOfMajorTicks = 3
    guard let startDate = fundValues.first?.date, let endDate = fundValues.last?.date else { return }
    
    // 生成中间的自动刻度(排除首尾,避免重复)
    let middleValues = AxisMarks.Position.automatic(desiredCount: numberOfMajorTicks - 2)
        .values(for: .value("Date", startDate), to: .value("Date", endDate))
        .compactMap { $0.dateValue }
    
    // 合并首尾+中间刻度,确保首尾必选
    let customMajorValues = [startDate] + middleValues + [endDate]
    
    // 主刻度与标签
    AxisMarks(values: .init(customMajorValues)) { value in
        AxisTick(length: 16, stroke: StrokeStyle(lineWidth: 1))
            .foregroundStyle(Color.black)
        AxisValueLabel(format: .dateTime.month().year(.twoDigits), verticalSpacing: 0)
    }
    
    // 次要刻度(每月),跳过主刻度位置避免重复
    AxisMarks(preset: .inset, values: .stride(by: .month)) { value in
        guard let date = value.dateValue, !customMajorValues.contains(date) else { return }
        AxisTick(centered: true, length: 8, stroke: StrokeStyle(lineWidth: 1))
            .foregroundStyle(Color.black)
    }
}

修改后的完整代码

import SwiftUI
import Charts

struct PlanValue: Codable, Identifiable {
    let value: Double
    let date: Date

    var id: Date { date }
}

// Date formatter to parse the date strings
let dateFormatter: ISO8601DateFormatter = {
    let formatter = ISO8601DateFormatter()
    formatter.formatOptions = [.withFullDate]
    return formatter
}()

let fundValues: [PlanValue] = [
    PlanValue(value: 100.58, date: dateFormatter.date(from: "2023-01-30")!),
    PlanValue(value: 200.22, date: dateFormatter.date(from: "2023-01-31")!),
    PlanValue(value: 505.66, date: dateFormatter.date(from: "2023-02-28")!),
    PlanValue(value: 399.33, date: dateFormatter.date(from: "2023-03-31")!),
    PlanValue(value: 5652.37, date: dateFormatter.date(from: "2023-04-30")!),
    PlanValue(value: 5755.66, date: dateFormatter.date(from: "2023-05-31")!),
    PlanValue(value: 5850.81, date: dateFormatter.date(from: "2023-06-30")!),
    PlanValue(value: 6002.82, date: dateFormatter.date(from: "2023-07-31")!),
    PlanValue(value: 5750.62, date: dateFormatter.date(from: "2023-08-31")!),
    PlanValue(value: 5699.93, date: dateFormatter.date(from: "2023-09-30")!),
    PlanValue(value: 5820.01, date: dateFormatter.date(from: "2023-10-31")!),
    PlanValue(value: 6105.03, date: dateFormatter.date(from: "2023-11-30")!),
    PlanValue(value: 6291.02, date: dateFormatter.date(from: "2023-12-31")!),
    PlanValue(value: 6399.48, date: dateFormatter.date(from: "2024-01-31")!),
    PlanValue(value: 6544.99, date: dateFormatter.date(from: "2024-02-29")!),
    PlanValue(value: 6700.69, date: dateFormatter.date(from: "2024-03-31")!),
    PlanValue(value: 6850.57, date: dateFormatter.date(from: "2024-04-30")!),
    PlanValue(value: 6998.78, date: dateFormatter.date(from: "2024-05-31")!),
    PlanValue(value: 6400.39, date: dateFormatter.date(from: "2024-06-30")!),
    PlanValue(value: 6450.33, date: dateFormatter.date(from: "2024-07-31")!),
    PlanValue(value: 29555.39, date: dateFormatter.date(from: "2024-08-31")!),
    PlanValue(value: 30300.39, date: dateFormatter.date(from: "2024-09-30")!),
    PlanValue(value: 30836.33, date: dateFormatter.date(from: "2024-10-31")!),
    PlanValue(value: 30750.06, date: dateFormatter.date(from: "2024-11-30")!),
    PlanValue(value: 31011.97, date: dateFormatter.date(from: "2024-12-31")!),
    PlanValue(value: 32500.24, date: dateFormatter.date(from: "2025-01-29")!)
]

struct ContentView: View {
    var body: some View {
        Chart {
            // Fund Values
            ForEach(fundValues) { planValue in
                LineMark(x: .value("Date", planValue.date),
                         y: .value("Value", planValue.value)
                )
            }
        }
        .chartXAxis {
            let numberOfMajorTicks = 3
            guard let startDate = fundValues.first?.date, let endDate = fundValues.last?.date else { return }
            
            // 生成中间的自动刻度(排除首尾,避免重复)
            let middleValues = AxisMarks.Position.automatic(desiredCount: numberOfMajorTicks - 2)
                .values(for: .value("Date", startDate), to: .value("Date", endDate))
                .compactMap { $0.dateValue }
            
            // 合并首尾+中间刻度,确保首尾必选
            let customMajorValues = [startDate] + middleValues + [endDate]
            
            // 主刻度与标签
            AxisMarks(values: .init(customMajorValues)) { value in
                AxisTick(length: 16, stroke: StrokeStyle(lineWidth: 1))
                    .foregroundStyle(Color.black)
                AxisValueLabel(format: .dateTime.month().year(.twoDigits), verticalSpacing: 0)
            }
            
            // 次要刻度(每月),跳过主刻度位置避免重复
            AxisMarks(preset: .inset, values: .stride(by: .month)) { value in
                guard let date = value.dateValue, !customMajorValues.contains(date) else { return }
                AxisTick(centered: true, length: 8, stroke: StrokeStyle(lineWidth: 1))
                    .foregroundStyle(Color.black)
            }
        }
        .chartYAxis {
            AxisMarks(preset: .aligned, values: .automatic(desiredCount: 7) ) { value in
                let isFirstLine = value.index == 0
                AxisGridLine(stroke: StrokeStyle(lineWidth: 1, dash: [isFirstLine ? 0 : 4], dashPhase: 0))
                    .foregroundStyle(isFirstLine ? .black : .gray)
                AxisValueLabel(format: .currency(code: "GBP").notation(.compactName))
            }
        }

        .frame(height: 275)
        .padding()
    }
}

#Preview {
    ContentView()
}

内容的提问来源于stack exchange,提问作者bencallis

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 13:05:55