mcpField
C
charts-3d

charts-3d

github.com/rshankras数据与数据库83876 次安装14 周前更新
代码仓库

关于 charts-3d

面向 Swift Charts 开发者的 3D 可视化与常规图表指南,涵盖曲面、相机交互、样式、图表构建、选择和无障碍设计。

主要功能

  1. 01数学函数与数据驱动曲面
  2. 02交互式相机姿态控制
  3. 03曲面渐变与着色
  4. 042D 图表标记、坐标轴与选择
  5. 05图表无障碍设计模式

适用场景

  1. 01将数学函数绘制为 3D 曲面
  2. 02在同一图表中比较多个曲面
  3. 03构建支持选择交互的 2D 图表
  4. 04让 VoiceOver 访问图表数据

安装

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

$npx skills add https://github.com/rshankras/claude-code-apple-skills --skill charts-3d

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

详细介绍

使用 Swift Charts 创建 3D 图表

使用 Chart3D 和 SurfacePlot 创建 3D 数据可视化。涵盖数学驱动的曲面、数据驱动的曲面、交互式相机姿态控制、曲面样式和相机投影模式。

此技能的触发场景

在以下情况下使用此技能:

  • 想要创建 3D 图表或 3D 数据可视化
  • 询问 Chart3D、SurfacePlot 或 3D 曲面图
  • 需要将数学函数可视化为 3D 曲面
  • 想要在 3D 图表上实现交互式拖动旋转
  • 询问 3D 图表的相机角度、姿态或投影
  • 需要使用渐变或基于高度的颜色为 3D 曲面设置样式
  • 想要在单个 3D 图表中渲染多个曲面
  • 询问如何根据点数组创建数据驱动的 3D 图
  • 需要 2D Swift Charts API 层:标记、坐标轴、系列样式、选择、SectorMark 饼图/圆环图或可滚动图表

决策树

What 3D chart feature do you need?
|
+-- Visualize a math function f(x, y) -> z
|   +-- Use SurfacePlot(x:y:z:function:)
|
+-- Visualize data points as a surface
|   +-- Use Chart3D(data) { point in SurfacePlot(...) }
|
+-- Interactive drag-to-rotate
|   +-- Bind pose: .chart3DPose($pose) with @State var pose: Chart3DPose
|
+-- Fixed viewing angle (no interaction)
|   +-- Read-only pose: .chart3DPose(Chart3DPose.front) or custom
|
+-- Style the surface color
|   +-- Solid color -> .foregroundStyle(Color.blue)
|   +-- Gradient -> .foregroundStyle(LinearGradient(...))
|   +-- Height-based -> .foregroundStyle(.heightBased(gradient, yRange:))
|   +-- Normal-based -> .foregroundStyle(.normalBased)
|
+-- Camera projection
|   +-- Perspective (depth) -> .chart3DCameraProjection(.perspective)
|   +-- Orthographic (flat) -> .chart3DCameraProjection(.orthographic)
|   +-- System default -> .chart3DCameraProjection(.automatic)
|
+-- Multiple surfaces in one chart
    +-- Place multiple SurfacePlot calls inside a single Chart3D { }

API 可用性

API最低版本导入备注
Chart3DiOS 26 / macOS 26Charts主要的 3D 图表容器
SurfacePlotiOS 26 / macOS 26Charts3D 曲面标记
Chart3DPoseiOS 26 / macOS 26Charts视角控制
Chart3DCameraProjectioniOS 26 / macOS 26Charts.automatic、.perspective、.orthographic
Chart3DSurfaceStyleiOS 26 / macOS 26Charts.heightBased、.normalBased

快速入门

数学驱动的曲面

根据函数 f(x, y) -> z 渲染曲面:

import SwiftUI
import Charts

struct WaveSurfaceView: View {
    var body: some View {
        Chart3D {
            SurfacePlot(
                x: "X",
                y: "Height",
                z: "Z",
                function: { x, z in
                    sin(x) * cos(z)
                }
            )
            .foregroundStyle(.blue)
        }
    }
}
数据驱动的曲面

根据数据点数组渲染曲面:

import SwiftUI
import Charts

struct DataPoint: Identifiable {
    let id = UUID()
    let x: Double
    let y: Double
    let z: Double
}

struct DataSurfaceView: View {
    let points: [DataPoint]

    var body: some View {
        Chart3D(points) { point in
            SurfacePlot(
                x: .value("X", point.x),
                y: .value("Height", point.y),
                z: .value("Z", point.z)
            )
        }
    }
}
交互式旋转

允许用户拖动以旋转图表:

import SwiftUI
import Charts

struct InteractiveChartView: View {
    @State private var pose = Chart3DPose.default

    var body: some View {
        Chart3D {
            SurfacePlot(
                x: "X",
                y: "Height",
                z: "Z",
                function: { x, z in
                    sin(x) * cos(z)
                }
            )
            .foregroundStyle(.blue)
        }
        .chart3DPose($pose)
    }
}

曲面样式模式

纯色
SurfacePlot(x: "X", y: "Y", z: "Z", function: { x, z in x * z })
    .foregroundStyle(.blue)
线性渐变
SurfacePlot(x: "X", y: "Y", z: "Z", function: { x, z in x * z })
    .foregroundStyle(
        LinearGradient(
            colors: [.blue, .green, .yellow],
            startPoint: .bottom,
            endPoint: .top
        )
    )
基于高度的曲面样式

根据高度值为曲面着色,在 y 轴范围内映射渐变:

SurfacePlot(x: "X", y: "Y", z: "Z", function: { x, z in sin(x) * cos(z) })
    .foregroundStyle(
        Chart3DSurfaceStyle.heightBased(
            Gradient(colors: [.blue, .cyan, .green, .yellow, .red]),
            yRange: -1...1
        )
    )
基于法线的曲面样式

根据曲面法线着色,呈现受光照影响的外观:

SurfacePlot(x: "X", y: "Y", z: "Z", function: { x, z in sin(x) * cos(z) })
    .foregroundStyle(Chart3DSurfaceStyle.normalBased)
曲面粗糙度

控制曲面的光泽度(0 = 反光,1 = 哑光):

SurfacePlot(x: "X", y: "Y", z: "Z", function: { x, z in sin(x) * cos(z) })
    .foregroundStyle(.blue)
    .roughness(0.3)

交互式姿态控制

预设姿态

Chart3DPose 提供常见视角的内置预设:

.chart3DPose(.default)   // Standard 3/4 angle
.chart3DPose(.front)     // Viewing from front
.chart3DPose(.back)      // Viewing from back
.chart3DPose(.top)       // Top-down view
.chart3DPose(.bottom)    // Bottom-up view
.chart3DPose(.right)     // Right side view
.chart3DPose(.left)      // Left side view
自定义姿态

指定精确的方位角(水平旋转)和倾角(垂直倾斜):

.chart3DPose(
    Chart3DPose(azimuth: .degrees(45), inclination: .degrees(30))
)
反模式
// ❌ Passing a literal where a binding is needed for interactivity
.chart3DPose(.default) // This is read-only; drag gestures will not work

// ✅ Use a @State binding for interactive rotation
@State private var pose = Chart3DPose.default
// ...
.chart3DPose($pose)

相机投影

控制 3D 深度的渲染方式:

Chart3D {
    SurfacePlot(x: "X", y: "Y", z: "Z", function: { x, z in sin(x) * cos(z) })
        .foregroundStyle(.blue)
}
.chart3DCameraProjection(.perspective)   // Objects farther away appear smaller
// .chart3DCameraProjection(.orthographic)  // No perspective distortion
// .chart3DCameraProjection(.automatic)     // System decides

多个曲面

在单个图表中渲染多个曲面以便比较:

import SwiftUI
import Charts

struct ComparisonChartView: View {
    @State private var pose = Chart3DPose.default

    var body: some View {
        Chart3D {
            SurfacePlot(
                x: "X",
                y: "Wave A",
                z: "Z",
                function: { x, z in sin(x) * cos(z) }
            )
            .foregroundStyle(.blue.opacity(0.8))

            SurfacePlot(
                x: "X",
                y: "Wave B",
                z: "Z",
                function: { x, z in cos(x) * sin(z) }
            )
            .foregroundStyle(.red.opacity(0.8))
        }
        .chart3DPose($pose)
        .chart3DCameraProjection(.perspective)
    }
}

完整示例

一个功能齐全的 3D 图表,包含基于高度的着色、交互式旋转和透视投影:

import SwiftUI
import Charts

struct TerrainView: View {
    @State private var pose = Chart3DPose(
        azimuth: .degrees(30),
        inclination: .degrees(25)
    )

    var body: some View {
        VStack {
            Text("Terrain Visualization")
                .font(.headline)

            Chart3D {
                SurfacePlot(
                    x: "Longitude",
                    y: "Elevation",
                    z: "Latitude",
                    function: { x, z in
                        let distance = sqrt(x * x + z * z)
                        return sin(distance) / max(distance, 0.1)
                    }
                )
                .foregroundStyle(
                    Chart3DSurfaceStyle.heightBased(
                        Gradient(colors: [
                            .blue, .cyan, .green, .yellow, .orange, .red
                        ]),
                        yRange: -0.5...1.0
                    )
                )
                .roughness(0.4)
            }
            .chart3DPose($pose)
            .chart3DCameraProjection(.perspective)
        }
        .padding()
    }
}

图表设计基础(WWDC22)

这些原则适用于你构建的每个图表,无论是 2D 还是 3D。三大支柱:聚焦、易用、无障碍。

  • 根据目标选择标记:用条形图呈现模式、范围和单个值;用点图发现离群值;用折线图呈现变化率。
  • 尽早使用真实且嘈杂的数据进行测试——占位用的正弦波会掩盖布局问题。明确设计边缘情况(例如,折线图中数据缺失时的间隙)。
  • 约 4 条水平网格线是不错的基准;选择直观的间隔——计数轴用 20 的倍数,按天划分的轴用 7 的倍数。
  • 标题应概括图表结论(“总销售额:1,234 个煎饼”),而不是使用通用标签(“销售额”);并为图表提供上下文。
  • 触控目标应延伸至图表的完整高度——点按条形所在的任意位置都能选中该条,而不只是绘制出的像素。
  • 同等支持触控、键盘、Voice Control、Switch Control 和 VoiceOver——每种交互都需要无需指针的操作方式。
  • 颜色用于增强,而非单独传达信息——先用符号或形状区分系列,再用颜色。检查深色、浅色和增强对比度模式下的对比度,并平衡饱和度,避免某个系列在视觉上压过其他系列。
  • 每种视觉编码都需要非视觉表示——为每个标记提供 VoiceOver 标签和 Audio Graph 描述符;Swift Charts 默认提供两者,因此不要用自定义绘制破坏它们。自定义绘制的图表需要手动实现这些功能——参见下文“Audio Graphs: AXChartDescriptor”。

Swift Charts 构建参考(2D)

上述设计基础适用于每个图表;以下是标准 2D 图表的 API 层(除非另有说明,适用于 iOS 16 及以上版本;选择、SectorMark 和滚动功能适用于 iOS 17 及以上版本)。

标记组合

Chart 由多个标记组合而成——BarMark、LineMark、PointMark、AreaMark、RuleMark、RectangleMark。.value("Label", v) 工厂参数有双重作用:它们会绑定数据,也会驱动自动生成的坐标轴和图例,因此请使用有意义的标签:

Chart(salesData) { sale in          // Identifiable data — no explicit ForEach needed
    BarMark(
        x: .value("Day", sale.day, unit: .day),   // unit: .day buckets temporal values per day
        y: .value("Sales", sale.count)
    )
}
  • 交换 x/y 值对即可转换图表方向——水平条形图无需其他改动。
  • unit:(.day、.month、.hour)控制时间分桶;如果省略,每个时间戳都会占据一个独立位置。
系列:样式与符号,不要只用颜色

.foregroundStyle(by:) 会将标记划分为不同系列;配合 .symbol(by:) 使用,即使不依赖颜色,系列也能保持易于区分(WWDC22):

Chart(data) { point in
    LineMark(x: .value("Day", point.day), y: .value("Sales", point.sales))
        .foregroundStyle(by: .value("City", point.city))
        .symbol(by: .value("City", point.city))
}
  • 使用 .chartForegroundStyleScale(["Cupertino": .indigo, "San Francisco": .teal]) 重新映射系列颜色。
  • .position(by: .value("City", point.city)) 可将堆叠条形图转换为分组条形图。
固定比例尺以保持稳定

固定比例尺,避免筛选数据集后整个图表发生跳动:

.chartYScale(domain: 0...maxExpectedSales)
.chartXScale(domain: startDate...endDate)
将统计数据组合到图表中

标记可以自由组合,因此汇总统计只是更多标记:

Chart {
    ForEach(data) { point in
        AreaMark(                                    // min–max band
            x: .value("Day", point.day),
            yStart: .value("Min", point.min),
            yEnd: .value("Max", point.max)
        )
        .opacity(0.3)

        LineMark(x: .value("Day", point.day), y: .value("Average", point.average))
    }

    RuleMark(y: .value("Overall", overallAverage))   // overall average line
        .foregroundStyle(.secondary)
        .annotation(position: .top, alignment: .leading) {
            Text("Avg: \(overallAverage, format: .number.precision(.fractionLength(0)))")
                .font(.caption)
                .foregroundStyle(.secondary)
        }
}
坐标轴使用构建器
.chartXAxis {
    AxisMarks(values: .stride(by: .month)) { value in
        AxisGridLine()
        AxisTick()
        AxisValueLabel(format: .dateTime.month(.narrow))
    }
}
.chartYAxis {
    AxisMarks(position: .leading)   // move the value axis to the leading edge
}
  • 条件样式应放在构建器内部——例如,在闭包中检查 value.as(Date.self),只将每个季度的第一个月份设为粗体。
  • .chartXAxis(.hidden) 会彻底移除坐标轴;.chartPlotStyle { $0.frame(height: 200).background(.gray.opacity(0.05)).border(.quaternary) } 可设置绘图区本身的尺寸和样式。
交互:优先使用选择功能

优先使用内置的选择绑定,而不是手动实现覆盖层手势(iOS 17):

@State private var selectedDay: Date?

Chart(data) { ... }
    .chartXSelection(value: $selectedDay)

将选择结果渲染为标记——使用 zIndex(-1) 的 RuleMark,使其绘制在数据后方,并添加保持在绘图区内的注释:

if let selectedDay {
    RuleMark(x: .value("Selected", selectedDay, unit: .day))
        .foregroundStyle(.gray.opacity(0.3))
        .zIndex(-1)
        .annotation(
            position: .top, spacing: 0,
            overflowResolution: .init(x: .fit(to: .chart), y: .disabled)
        ) {
            SelectionDetailCard(day: selectedDay)
        }
}

如需完全自定义命中测试,可在 .chartOverlay 中使用 GeometryReader 调用 ChartProxy:proxy.value(atX:) 会将手势位置转换为数据值,proxy.position(forX:) 则会反向转换。

SectorMark:饼图和圆环图(iOS 17 及以上版本)
Chart(data) { item in
    SectorMark(
        angle: .value("Sales", item.sales),
        innerRadius: .ratio(0.62),     // donut hole
        angularInset: 1.5              // 1.5 per side = 3pt gaps between sectors
    )
    .cornerRadius(4)
    .foregroundStyle(by: .value("Name", item.name))
}
.chartBackground { proxy in
    GeometryReader { geo in            // headline metric in the donut hole
        if let anchor = proxy.plotFrame {
            let frame = geo[anchor]
            Text("Best: \(topSellerName)")
                .position(x: frame.midX, y: frame.midY)
        }
    }
}
大型数据集:滚动查看窗口

不要试图在一个屏幕中塞入一整年的数据——显示一个窗口并允许滚动(WWDC23):

Chart(yearOfData) { ... }
    .chartScrollableAxes(.horizontal)
    .chartXVisibleDomain(length: 3600 * 24 * 30)   // 30-day window
    .chartScrollPosition(x: $scrollDate)           // read/write the scroll offset
    .chartScrollTargetBehavior(
        .valueAligned(
            matching: DateComponents(hour: 0),                    // land on day boundaries
            majorAlignment: .matching(DateComponents(day: 1))     // snap paging to month starts
        )
    )
无障碍功能:每个标记

VoiceOver 自动生成的描述会朗读原始值;为每个标记添加标签可覆盖这些描述:

BarMark(x: .value("Day", sale.day, unit: .day), y: .value("Sales", sale.count))
    .accessibilityLabel(sale.day.formatted(date: .abbreviated, time: .omitted))
    .accessibilityValue("\(sale.count) pancakes sold")

对于数百或数千个点,应将其归入合理的区间,并为每个区间而非每个点提供一个无障碍元素(WWDC21 10122)。

音频图表:AXChartDescriptor(WWDC21 10122)

音频图表可让 VoiceOver 将数据系列作为连续音调播放——音高 = Y 值,时间 = X 位置——并提供探索器视图(转子 →“音频图表”→“图表详细信息”),用于播放数据转化为声音的内容、拖动浏览(双击并按住;暂停时会朗读当前位置的值),以及显示自动计算的摘要统计数据。Swift Charts 会生成默认描述符;自定义绘制的图表需遵循 AXChart(UIKit),或在 SwiftUI(iOS 15+)中使用 .accessibilityChartDescriptor(_:) 修饰符和 AXChartDescriptorRepresentable:

var accessibilityChartDescriptor: AXChartDescriptor? {
    let xAxis = AXNumericDataAxisDescriptor(
        title: "Cups of coffee",
        range: 0...10,
        gridlinePositions: [],                              // gridlines render as haptics during playback
        valueDescriptionProvider: { "\(Int($0)) cups" })    // "5 cups", never a bare "5"

    let yAxis = AXNumericDataAxisDescriptor(
        title: "Lines of code",
        range: 0...100,
        gridlinePositions: [],
        valueDescriptionProvider: { "\(Int($0)) lines of code" })

    let series = AXDataSeriesDescriptor(
        name: "Productivity",
        isContinuous: true,      // line → one continuous tone; false for bars/points → discrete tones
        dataPoints: model.points.map { AXDataPoint(x: $0.x, y: $0.y) })

    return AXChartDescriptor(
        title: model.title,
        summary: model.summary,  // 1–2 sentence alt text; spoken in the explorer view
        xAxis: xAxis,
        yAxis: yAxis,
        additionalAxes: [],
        series: [series])
}
  • 分类轴使用 AXCategoricalDataAxisDescriptor;在生产环境中,应本地化并正确处理 valueDescriptionProvider 输出的复数形式。
  • 所有图表的视觉最低标准(WWDC21 10122):前景与背景对比度 ≥ 4.5:1(使用 Accessibility Inspector 的 Color Contrast Calculator 验证);切勿搭配红色和绿色(最常见的色盲类型),并避免搭配蓝色和黄色(第二常见的色盲类型);除颜色外还要使用符号,确保即使完全无法辨别颜色也能区分各个系列。

常见错误

#错误修正方法
1忘记 import Charts必须同时导入 SwiftUI 和 Charts
2使用 .chart3DPose(.default) 并期望可通过拖动旋转使用 @State 绑定:.chart3DPose($pose),以启用交互式旋转
3设置的 yRange 未涵盖函数的实际输出使 .heightBased() 中的 yRange 与函数实际输出的最小值和最大值相符
4未应用 .foregroundStyle() 就使用 .roughness()Roughness 会修改现有表面外观;请先设置前景样式
5在演示或展示场景中使用正交投影视觉效果优先时,建议使用 .perspective;需要精确读取数据时,使用 .orthographic

检查清单

导入和设置
  • 已同时导入 import SwiftUI 和 import Charts
  • 部署目标为 iOS 26 / macOS 26 或更高版本
  • 所有 SurfacePlot 内容都由 Chart3D 包裹
表面配置
  • 坐标轴标签(x:、y:、z:)清晰且有意义
  • 每个 SurfacePlot 都应用了 foregroundStyle,以便清楚地区分
  • .heightBased() 中的 yRange 与函数的实际输出范围相符
  • roughness 值适合使用场景(0 = 反光,1 = 哑光)
交互
  • 如果需要拖动旋转,Pose 应为 @State 绑定
  • 如果需要锁定旋转,Pose 应为常量(非绑定)
  • 初始 Pose 角度能为数据提供良好的默认视图
摄像机
  • 摄像机投影设置合适(视觉效果优先时使用 .perspective,精度优先时使用 .orthographic)
  • 如果使用 .automatic,已确认系统的选择效果可以接受
多个表面
  • 每个表面都有不同的颜色或不透明度,以便区分
  • 各表面的坐标轴标签彼此不同,或在适当时共用
无障碍功能
  • 对比度 ≥ 4.5:1;不搭配红色与绿色或蓝色与黄色的系列;使用符号辅助颜色(WWDC21 10122)
  • 自定义绘制的图表提供 AXChartDescriptor(音频图表)——或保留 Swift Charts 的默认设置
  • 大型数据集按区间而非按点提供无障碍元素

References

评论

支持 Markdown