swift-charts

swift-charts

热门

使用 Swift Charts 实现、审查或改进数据可视化。适用于构建柱状图、折线图、面积图、散点图、饼图、环形图或 iOS 26 的 3D 图表;添加图表选择、滚动、标注、坐标轴、比例尺、图例或 foregroundStyle 分组;使用 BarPlot、LinePlot、AreaPlot、PointPlot、Chart3D 或 SurfacePlot 绘制函数;或创建热力图、甘特图、分组柱状图、迷你图、阈值线或空间可视化。

932Star
47Fork
更新于 2026/7/15
SKILL.md
只读
名称
swift-charts
描述

使用 Swift Charts 实现、审查或改进数据可视化。适用于构建柱状图、折线图、面积图、散点图、饼图、环形图或 iOS 26 的 3D 图表;添加图表选择、滚动、标注、坐标轴、比例尺、图例或 foregroundStyle 分组;使用 BarPlot、LinePlot、AreaPlot、PointPlot、Chart3D 或 SurfacePlot 绘制函数;或创建热力图、甘特图、分组柱状图、迷你图、阈值线或空间可视化。

Swift Charts

使用 Swift Charts 构建数据可视化,目标平台 iOS 26+。在 ChartChart3D 内部组合标记(marks),通过视图修饰符配置坐标轴和比例尺,并在数据需要时使用向量化绘图或 3D 绘图。

有关扩展模式、3D 图表、无障碍和主题指南,请参阅 references/charts-patterns.md

目录

工作流程

1. 构建新图表

  1. 将数据定义为 Identifiable 结构体,或使用 id: 键路径。
  2. 选择标记类型:BarMarkLineMarkPointMarkAreaMarkRuleMarkRectangleMarkSectorMarkSurfacePlot
  3. 将 2D 标记包裹在 Chart 中;仅对真正的空间或曲面数据使用 Chart3D
  4. 编码视觉通道:.foregroundStyle(by:).symbol(by:).lineStyle(by:)
  5. 使用 .chartXAxis / .chartYAxis 配置坐标轴。
  6. 使用 .chartXScale(domain:) / .chartYScale(domain:) 设置比例尺域。
  7. 根据需要添加选择、滚动或标注。
  8. 对于 1000+ 个 2D 数据点,使用向量化绘图(BarPlotLinePlot 等)。
  9. 渲染具有代表性的空状态、典型状态、密集状态、大文本、高对比度和 VoiceOver 状态;如果存在选择和滚动,则进行测试。
  10. 如果编码、坐标轴、选择或无障碍检查失败,则恢复数据夹具,修复一层,并在添加装饰之前重新运行相同的矩阵。

2. 审查现有图表代码

在判断标记选择之前,先确定数据语义。然后追踪每个值通过标记、比例尺、坐标轴、样式、选择和无障碍输出的过程;运行与新图表相同的验证矩阵。

图表容器

Chart(sales) { item in
    BarMark(x: .value("Month", item.month), y: .value("Revenue", item.revenue))
}

对单个集合使用数据驱动初始化器。对混合标记或多个系列使用内容闭包,并在元素不是 Identifiable 时传递 id:。加载 references/charts-patterns.md 获取完整的混合系列、主题、无障碍和 3D 示例。

标记类型

BarMark (iOS 16+)

// 垂直柱状图
BarMark(x: .value("Month", item.month), y: .value("Sales", item.sales))

// 按类别堆叠(当相同 x 映射到多个柱时自动发生)
BarMark(x: .value("Month", item.month), y: .value("Sales", item.sales))
    .foregroundStyle(by: .value("Product", item.product))

// 水平柱状图
BarMark(x: .value("Sales", item.sales), y: .value("Month", item.month))

// 区间柱状图(甘特图)
BarMark(
    xStart: .value("Start", item.start),
    xEnd: .value("End", item.end),
    y: .value("Task", item.task)
)

LineMark (iOS 16+)

// 单条折线
LineMark(x: .value("Date", item.date), y: .value("Price", item.price))

// 通过 foregroundStyle 编码的多系列折线
LineMark(x: .value("Date", item.date), y: .value("Temp", item.temp))
    .foregroundStyle(by: .value("City", item.city))
    .interpolationMethod(.catmullRom)

// 使用显式 series 参数的多系列折线
LineMark(
    x: .value("Date", item.date),
    y: .value("Price", item.price),
    series: .value("Ticker", item.ticker)
)

PointMark (iOS 16+)

PointMark(x: .value("Height", item.height), y: .value("Weight", item.weight))
    .foregroundStyle(by: .value("Species", item.species))
    .symbol(by: .value("Species", item.species))
    .symbolSize(100)

AreaMark (iOS 16+)

// 堆叠面积图
AreaMark(x: .value("Date", item.date), y: .value("Sales", item.sales))
    .foregroundStyle(by: .value("Category", item.category))

// 范围带
AreaMark(
    x: .value("Date", item.date),
    yStart: .value("Min", item.min),
    yEnd: .value("Max", item.max)
)
.opacity(0.3)

RuleMark (iOS 16+)

RuleMark(y: .value("Target", 9000))
    .foregroundStyle(.red)
    .lineStyle(StrokeStyle(dash: [5, 3]))
    .annotation(position: .top, alignment: .leading) {
        Text("Target").font(.caption).foregroundStyle(.red)
    }

RectangleMark (iOS 16+)

RectangleMark(x: .value("Hour", item.hour), y: .value("Day", item.day))
    .foregroundStyle(by: .value("Intensity", item.intensity))

SectorMark (iOS 17+)

使用 SectorMark 表示严格正的值;在饼图或环形图外部过滤、聚合或解释零/负值。

// 饼图
Chart(data, id: \.name) { item in
    SectorMark(angle: .value("Sales", item.sales))
        .foregroundStyle(by: .value("Category", item.name))
}

// 环形图
Chart(data, id: \.name) { item in
    SectorMark(
        angle: .value("Sales", item.sales),
        innerRadius: .ratio(0.618),
        outerRadius: .inset(10),
        angularInset: 1
    )
    .cornerRadius(4)
    .foregroundStyle(by: .value("Category", item.name))
}

坐标轴自定义

// 隐藏坐标轴
.chartXAxis(.hidden)
.chartYAxis(.hidden)

// 自定义坐标轴内容
.chartXAxis {
    AxisMarks(values: .stride(by: .month)) { value in
        AxisGridLine()
        AxisTick()
        AxisValueLabel(format: .dateTime.month(.abbreviated))
    }
}

// 多个 AxisMarks 组合(网格和标签使用不同间隔)
.chartXAxis {
    AxisMarks(values: .stride(by: .day)) { _ in AxisGridLine() }
    AxisMarks(values: .stride(by: .week)) { _ in
        AxisTick()
        AxisValueLabel(format: .dateTime.week())
    }
}

// 坐标轴标签(标题)
.chartXAxisLabel("Time", position: .bottom, alignment: .center)
.chartYAxisLabel("Revenue ($)", position: .leading, alignment: .center)

比例尺配置

.chartYScale(domain: 0...100)                          // 显式数值域
.chartYScale(domain: .automatic(includesZero: true))   // 包含零
.chartYScale(domain: 1...10000, type: .log)            // 对数比例尺
.chartXScale(domain: ["Mon", "Tue", "Wed", "Thu"])     // 分类排序

前景样式与编码

BarMark(...).foregroundStyle(.blue)                                    // 静态颜色
BarMark(...).foregroundStyle(by: .value("Category", item.category))   // 数据编码
AreaMark(...).foregroundStyle(                                         // 渐变
    .linearGradient(colors: [.blue, .cyan], startPoint: .bottom, endPoint: .top)
)

选择(iOS 17+)

@State private var selectedDate: Date?
@State private var selectedRange: ClosedRange<Date>?
@State private var selectedAngle: Double?

// 点选择
Chart(data) { item in
    LineMark(x: .value("Date", item.date), y: .value("Value", item.value))
}
.chartXSelection(value: $selectedDate)

// 范围选择
.chartXSelection(range: $selectedRange)

// 角度选择绑定可绘制的角度值;从范围推导类别。
.chartAngleSelection(value: $selectedAngle)

可滚动图表(iOS 17+)

Chart(dailyData) { item in
    BarMark(x: .value("Date", item.date, unit: .day), y: .value("Steps", item.steps))
}
.chartScrollableAxes(.horizontal)
.chartXVisibleDomain(length: 3600 * 24 * 7) // 7 天可见
.chartScrollPosition(initialX: latestDate)
.chartScrollTargetBehavior(
    .valueAligned(matching: DateComponents(hour: 0), majorAlignment: .page)
)

标注

BarMark(x: .value("Month", item.month), y: .value("Sales", item.sales))
    .annotation(position: .top, alignment: .center, spacing: 4) {
        Text("\(item.sales, format: .number)").font(.caption2)
    }

// 溢出处理
.annotation(
    position: .top,
    overflowResolution: .init(x: .fit(to: .chart), y: .padScale)
) { Text("Label") }

图例

.chartLegend(.hidden)                                           // 隐藏
.chartLegend(position: .bottom, alignment: .center, spacing: 10) // 位置
.chartLegend(position: .bottom) {                                // 自定义
    HStack {
        ForEach(categories, id: \.self) { cat in
            Label(cat, systemImage: "circle.fill").font(.caption)
        }
    }
}

向量化绘图(iOS 18+)

用于大数据集(1000+ 点)。接受整个集合或函数。

// 数据驱动
Chart {
    BarPlot(sales, x: .value("Month", \.month), y: .value("Revenue", \.revenue))
        .foregroundStyle(\.barColor)
}

// 函数绘图:y = f(x)
Chart {
    LinePlot(x: "x", y: "y", domain: -5...5) { x in sin(x) }
}

// 参数方程:(x, y) = f(t)
Chart {
    LinePlot(x: "x", y: "y", t: "t", domain: 0...(2 * .pi)) { t in
        (x: cos(t), y: sin(t))
    }
}

在简单值修饰符之前应用基于 KeyPath 的修饰符:

BarPlot(data, x: .value("X", \.x), y: .value("Y", \.y))
    .foregroundStyle(\.color)    // 先 KeyPath
    .opacity(0.8)                // 后值修饰符

3D 图表(iOS 26+)

对空间数据或双变量曲面使用 Chart3D,不要将其作为普通 2D 分类或时间序列图表的装饰性替代品。Chart3D 接受 SurfacePlot 以及 PointMarkRuleMarkRectangleMark 的 3D 初始化器。

@State private var pose: Chart3DPose = .default

Chart3D {
    SurfacePlot(x: "x", y: "y", z: "z") { x, z in
        sin(2 * x) * cos(2 * z)
    }
    .foregroundStyle(.heightBased)
}
.chartXScale(domain: -2...2)
.chartYScale(domain: -1...1)
.chartZScale(domain: -2...2)
.chart3DPose($pose)

常见错误

1. 多折线图缺少 series 参数

// 错误——所有点连接成一条线
Chart {
    ForEach(allCities) { item in
        LineMark(x: .value("Date", item.date), y: .value("Temp", item.temp))
    }
}

// 正确——每个城市一条独立的线
Chart {
    ForEach(allCities) { item in
        LineMark(x: .value("Date", item.date), y: .value("Temp", item.temp))
            .foregroundStyle(by: .value("City", item.city))
    }
}

2. SectorMark 切片过多

// 错误——20 个小切片难以阅读
Chart(twentyCategories, id: \.name) { item in
    SectorMark(angle: .value("Value", item.value))
}

// 正确——分组为前 5 个 + "其他"
Chart(groupedData, id: \.name) { item in
    SectorMark(angle: .value("Value", item.value))
        .foregroundStyle(by: .value("Category", item.name))
}

3. 零基线重要时缺少比例尺域

// 错误——坐标轴从 ~95 开始;小变化看起来很大
Chart(data) {
    LineMark(x: .value("Day", $0.day), y: .value("Score", $0.score))
}

// 正确——显式域以诚实表示
Chart(data) {
    LineMark(x: .value("Day", $0.day), y: .value("Score", $0.score))
}
.chartYScale(domain: 0...100)

4. 静态 foregroundStyle 覆盖数据编码

// 错误——静态颜色覆盖了按值编码
BarMark(x: .value("X", item.x), y: .value("Y", item.y))
    .foregroundStyle(by: .value("Category", item.category))
    .foregroundStyle(.blue)

// 正确——仅使用数据编码
BarMark(x: .value("X", item.x), y: .value("Y", item.y))
    .foregroundStyle(by: .value("Category", item.category))

5. 对 10,000+ 数据点使用单个标记

// 错误——创建 10,000 个标记视图;性能慢
Chart(largeDataset) { item in
    PointMark(x: .value("X", item.x), y: .value("Y", item.y))
}

// 正确——向量化绘图(iOS 18+)
Chart {
    PointPlot(largeDataset, x: .value("X", \.x), y: .value("Y", \.y))
}

6. 固定图表高度破坏动态类型

// 错误——大文本时裁剪坐标轴标签
Chart(data) { ... }
    .frame(height: 200)

// 正确——自适应大小
Chart(data) { ... }
    .frame(minHeight: 200, maxHeight: 400)

7. 向量化绘图上值修饰符在 KeyPath 修饰符之前

// 错误——编译错误
BarPlot(data, x: .value("X", \.x), y: .value("Y", \.y))
    .opacity(0.8)
    .foregroundStyle(\.color)

// 正确——KeyPath 修饰符在前
BarPlot(data, x: .value("X", \.x), y: .value("Y", \.y))
    .foregroundStyle(\.color)
    .opacity(0.8)

8. 缺少无障碍标签

// 错误——VoiceOver 用户得不到上下文
Chart(data) {
    BarMark(x: .value("Month", $0.month), y: .value("Sales", $0.sales))
}

// 正确——为每个标记添加无障碍信息
Chart(data) { item in
    BarMark(x: .value("Month", item.month), y: .value("Sales", item.sales))
        .accessibilityLabel("\(item.month)")
        .accessibilityValue("\(item.sales) units sold")
}

9. 将角度选择视为类别选择

chartAngleSelection(value:) 绑定选中的可绘制角度值。对于饼图和环形图,在比较类别标签之前,需要将该数值通过累积扇形范围进行映射。

审查清单

  • [ ] 数据模型使用 Identifiable 或图表使用 id: 键路径
  • [ ] 标记类型与目标匹配(柱状图=比较,折线图=趋势,扇形=比例)
  • [ ] 多系列折线使用 series: 参数或 .foregroundStyle(by:)
  • [ ] 坐标轴配置了适当的标签、刻度和网格线
  • [ ] 当零基线重要时,显式设置比例尺域
  • [ ] 饼图/环形图使用正值,5-7 个扇形,并分组为“其他”
  • [ ] 选择绑定类型与坐标轴数据类型匹配(日期轴使用 Date?
  • [ ] 饼图/环形图的角度选择将数值角度映射回类别
  • [ ] 可滚动图表设置 .chartXVisibleDomain(length:) 用于视口
  • [ ] 数据集超过 1000 点时使用向量化绘图
  • [ ] 向量化绘图上 KeyPath 修饰符在值修饰符之前
  • [ ] Chart3D 仅用于真正的 3D 数据或曲面,并检查了 z 比例尺和姿态
  • [ ] 为标记添加了无障碍标签以支持 VoiceOver
  • [ ] 图表在动态类型和深色模式下测试
  • [ ] 图例可见并定位,或有意隐藏
  • [ ] 确保图表数据模型类型是 Sendable;在 @MainActor 上更新图表数据

参考资料