mcpField
S
swift-charts

swift-charts

github.com/dpearson2699数据与数据库1.2k4.0k 次安装13 周前更新
代码仓库

关于 swift-charts

面向 Swift 开发者的 Swift Charts 可视化技能,介绍二维与三维图表、标记、坐标轴、选择和滚动,并包含大数据集及无障碍检查建议。

主要功能

  1. 01组合多种图表标记
  2. 02配置坐标轴、比例尺和图例
  3. 03添加选择、滚动与注释
  4. 04使用向量化绘图处理大数据集
  5. 05构建空间数据三维图表
  6. 06检查无障碍与图表状态

适用场景

  1. 01展示分类比较和时间趋势
  2. 02为图表添加交互式数据选择
  3. 03绘制超过 1000 个数据点
  4. 04呈现空间数据或双变量曲面

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/dpearson2699/swift-ios-skills --skill swift-charts

安装后在 Claude Code 中输入/swift-charts确认已加载。

附带文件evals/evals.jsonreferences/charts-patterns.md

详细介绍

Swift Charts

使用 Swift Charts 构建数据可视化,目标平台为 iOS 26+。在 Chart 或 Chart3D 中组合标记,使用视图修饰符配置坐标轴和比例尺,并在数据适用时使用向量化绘图或 3D 绘图。

参见 references/charts-patterns.md,了解更多模式、3D 图表、无障碍功能和主题配置指南。

目录

工作流

1. 构建新图表
  1. 将数据定义为 Identifiable 结构体,或使用 id: 键路径。
  2. 选择标记类型:BarMark、LineMark、PointMark、AreaMark、 RuleMark、RectangleMark、SectorMark 或 SurfacePlot。
  3. 将 2D 标记放入 Chart;仅对真实的空间数据或曲面数据使用 Chart3D。
  4. 对视觉通道进行编码:.foregroundStyle(by:)、.symbol(by:)、.lineStyle(by:)。
  5. 使用 .chartXAxis / .chartYAxis 配置坐标轴。
  6. 使用 .chartXScale(domain:) / .chartYScale(domain:) 设置比例尺的取值范围。
  7. 按需添加选择、滚动或注释。
  8. 对于 1000 个以上的 2D 数据点,使用向量化绘图(BarPlot、LinePlot 等)。
  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+)
// Vertical bar
BarMark(x: .value("Month", item.month), y: .value("Sales", item.sales))

// Stacked by category (automatic when same x maps to multiple bars)
BarMark(x: .value("Month", item.month), y: .value("Sales", item.sales))
    .foregroundStyle(by: .value("Product", item.product))

// Horizontal bar
BarMark(x: .value("Sales", item.sales), y: .value("Month", item.month))

// Interval bar (Gantt chart)
BarMark(
    xStart: .value("Start", item.start),
    xEnd: .value("End", item.end),
    y: .value("Task", item.task)
)
LineMark(iOS 16+)
// Single line
LineMark(x: .value("Date", item.date), y: .value("Price", item.price))

// Multi-series via foregroundStyle encoding
LineMark(x: .value("Date", item.date), y: .value("Temp", item.temp))
    .foregroundStyle(by: .value("City", item.city))
    .interpolationMethod(.catmullRom)

// Multi-series with explicit series parameter
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+)
// Stacked area
AreaMark(x: .value("Date", item.date), y: .value("Sales", item.sales))
    .foregroundStyle(by: .value("Category", item.category))

// Range band
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 仅适用于严格大于零的值;对于饼图或圆环图之外的零值和负值,请进行筛选、聚合或说明。

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

// Donut chart
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))
}

坐标轴自定义

// Hide axes
.chartXAxis(.hidden)
.chartYAxis(.hidden)

// Custom axis content
.chartXAxis {
    AxisMarks(values: .stride(by: .month)) { value in
        AxisGridLine()
        AxisTick()
        AxisValueLabel(format: .dateTime.month(.abbreviated))
    }
}

// Multiple AxisMarks compositions (different intervals for grid vs. labels)
.chartXAxis {
    AxisMarks(values: .stride(by: .day)) { _ in AxisGridLine() }
    AxisMarks(values: .stride(by: .week)) { _ in
        AxisTick()
        AxisValueLabel(format: .dateTime.week())
    }
}

// Axis labels (titles)
.chartXAxisLabel("Time", position: .bottom, alignment: .center)
.chartYAxisLabel("Revenue ($)", position: .leading, alignment: .center)

比例尺配置

.chartYScale(domain: 0...100)                          // Explicit numeric domain
.chartYScale(domain: .automatic(includesZero: true))   // Include zero
.chartYScale(domain: 1...10000, type: .log)            // Logarithmic scale
.chartXScale(domain: ["Mon", "Tue", "Wed", "Thu"])     // Categorical ordering

前景样式和编码

BarMark(...).foregroundStyle(.blue)                                    // Static color
BarMark(...).foregroundStyle(by: .value("Category", item.category))   // Data encoding
AreaMark(...).foregroundStyle(                                         // Gradient
    .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?

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

// Range selection
.chartXSelection(range: $selectedRange)

// Angular selection binds the plottable angle value; derive the category from ranges.
.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 days visible
.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)
    }

// Overflow resolution
.annotation(
    position: .top,
    overflowResolution: .init(x: .fit(to: .chart), y: .padScale)
) { Text("Label") }

图例

.chartLegend(.hidden)                                           // Hide
.chartLegend(position: .bottom, alignment: .center, spacing: 10) // Position
.chartLegend(position: .bottom) {                                // Custom
    HStack {
        ForEach(categories, id: \.self) { cat in
            Label(cat, systemImage: "circle.fill").font(.caption)
        }
    }
}

向量化绘图(iOS 18+)

适用于大型数据集(1000 个以上的数据点)。可以接受整个集合或函数。

// Data-driven
Chart {
    BarPlot(sales, x: .value("Month", \.month), y: .value("Revenue", \.revenue))
        .foregroundStyle(\.barColor)
}

// Function plotting: y = f(x)
Chart {
    LinePlot(x: "x", y: "y", domain: -5...5) { x in sin(x) }
}

// Parametric: (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 first
    .opacity(0.8)                // Value modifier second

3D 图表(iOS 26+)

对空间数据或双变量曲面使用 Chart3D,不要将其作为普通 2D 分类图表或时间序列图表的装饰性替代。Chart3D 支持 SurfacePlot,以及 PointMark、RuleMark 和 RectangleMark 的 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. 多折线图缺少系列参数
// WRONG -- all points connect into one line
Chart {
    ForEach(allCities) { item in
        LineMark(x: .value("Date", item.date), y: .value("Temp", item.temp))
    }
}

// CORRECT -- separate lines per city
Chart {
    ForEach(allCities) { item in
        LineMark(x: .value("Date", item.date), y: .value("Temp", item.temp))
            .foregroundStyle(by: .value("City", item.city))
    }
}
2. SectorMark 切片过多
// WRONG -- 20 tiny sectors are unreadable
Chart(twentyCategories, id: \.name) { item in
    SectorMark(angle: .value("Value", item.value))
}

// CORRECT -- group into top 5 + "Other"
Chart(groupedData, id: \.name) { item in
    SectorMark(angle: .value("Value", item.value))
        .foregroundStyle(by: .value("Category", item.name))
}
3. 需要零基线时缺少比例尺取值范围
// WRONG -- axis starts at ~95; small changes look dramatic
Chart(data) {
    LineMark(x: .value("Day", $0.day), y: .value("Score", $0.score))
}

// CORRECT -- explicit domain for honest representation
Chart(data) {
    LineMark(x: .value("Day", $0.day), y: .value("Score", $0.score))
}
.chartYScale(domain: 0...100)
4. 静态 foregroundStyle 覆盖数据编码
// WRONG -- static color overrides by-value encoding
BarMark(x: .value("X", item.x), y: .value("Y", item.y))
    .foregroundStyle(by: .value("Category", item.category))
    .foregroundStyle(.blue)

// CORRECT -- use only the data encoding
BarMark(x: .value("X", item.x), y: .value("Y", item.y))
    .foregroundStyle(by: .value("Category", item.category))
5. 为 10,000 个以上的数据点分别创建标记
// WRONG -- creates 10,000 mark views; slow
Chart(largeDataset) { item in
    PointMark(x: .value("X", item.x), y: .value("Y", item.y))
}

// CORRECT -- vectorized plot (iOS 18+)
Chart {
    PointPlot(largeDataset, x: .value("X", \.x), y: .value("Y", \.y))
}
6. 固定图表高度导致动态字体无法正常工作
// WRONG -- clips axis labels at large text sizes
Chart(data) { ... }
    .frame(height: 200)

// CORRECT -- adaptive sizing
Chart(data) { ... }
    .frame(minHeight: 200, maxHeight: 400)
7. 向量化绘图中的值修饰符之后使用 KeyPath 修饰符
// WRONG -- compiler error
BarPlot(data, x: .value("X", \.x), y: .value("Y", \.y))
    .opacity(0.8)
    .foregroundStyle(\.color)

// CORRECT -- KeyPath modifiers first
BarPlot(data, x: .value("X", \.x), y: .value("Y", \.y))
    .foregroundStyle(\.color)
    .opacity(0.8)
8. 缺少辅助功能标签
// WRONG -- VoiceOver users get no context
Chart(data) {
    BarMark(x: .value("Month", $0.month), y: .value("Sales", $0.sales))
}

// CORRECT -- add per-mark accessibility
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 修饰符,再应用值修饰符
  • 仅在处理真实 3D 数据或曲面时使用 Chart3D,并检查 z 轴比例尺和视角
  • 为标记添加辅助功能标签,以支持 VoiceOver
  • 使用动态字体和深色模式测试图表
  • 显示并定位图例,或有意将其隐藏
  • 确保图表数据模型类型符合 Sendable;在 @MainActor 上更新图表数据

参考资料

评论

支持 Markdown