WWDC Quick Look 💓 By SwiftGGTeam
Bring Swift Charts to the third dimension

Bring Swift Charts to the third dimension

观看原视频

Highlight

Swift Charts 在 iOS 26、macOS 26 和 visionOS 26 上新增 Chart3DSurfacePlot,把 PointMark 加上 z 轴即可获得可旋转的 3D 散点图。


核心内容

企鹅数据集里有三个维度:喙长、鳍长、体重。要想看清三个物种(Chinstrap、Gentoo、Adélie)之间的差异,只能画三张 2D 散点图——一张鳍长 vs 体重,一张喙长 vs 体重,一张喙长 vs 鳍长。每张图都只能展示两个属性的关系,开发者要在脑子里把三张图拼起来才能得到一个完整的认知。这种「分屏分析」的成本很高,尤其当数据本身就是三维的时候,2D 投影会丢失关键的聚类形状。

iOS 26、macOS 26 和 visionOS 26 给 Swift Charts 加了 3D 支持。把 Chart 改成 Chart3D,给 PointMark 加一个 Z 值,三张 2D 图就压缩成了一张可旋转的 3D 散点图。三个物种立刻分成三团,从侧面看又能退化回任意一对 2D 关系。新引入的 SurfacePlotLinePlot 的三维版本,接受一个 (Double, Double) -> Double 闭包,在 XZ 平面上采样生成连续曲面,刚好可以叠在散点上做线性回归。


详细内容

把 2D 升到 3D,API 改动小到只有三处:Chart 换成 Chart3DPointMark 多一个 z 参数、轴标签多一个 chartZAxisLabel03:28):

struct PenguinChart: View {
  var body: some View {
    Chart3D(penguins) { penguin in
      PointMark(
        x: .value("Flipper Length", penguin.flipperLength),
        y: .value("Weight", penguin.weight),
        z: .value("Beak Length", penguin.beakLength)
      )
      .foregroundStyle(by: .value("Species", penguin.species))
    }
    .chartXAxisLabel("Flipper Length (mm)")
    .chartYAxisLabel("Weight (kg)")
    .chartZAxisLabel("Beak Length (mm)")
    .chartXScale(domain: 160...240, range: -0.5...0.5)
    .chartYScale(domain: 2...7, range: -0.5...0.5)
    .chartZScale(domain: 30...60, range: -0.5...0.5)
  }
}

关键点:

  • Chart3D 直接替换 Chart,构造器签名一致,老代码迁移成本低。
  • PointMark 增加第三个参数 z,类型仍是 PlottableValue,配合 foregroundStyle(by:) 就能按 Species 自动配色。
  • chartXScale / chartYScale / chartZScale 都接受 domainrangedomain 是数据范围,range 是图表内归一化坐标(这里都是 -0.5...0.5),保证三轴几何比例一致。

SurfacePlot 是三维独有的新 mark,签名形如 SurfacePlot(x:y:z:_:),闭包是 (Double, Double) -> Double,对每个 (x, z) 计算出 y(05:19):

Chart3D {
  SurfacePlot(x: "X", y: "Y", z: "Z") { x, z in
    (sin(5 * x) + sin(5 * z)) / 2
  }
}

关键点:

  • 三个字符串参数 "X" / "Y" / "Z" 是轴标识,对应 chartXScale 等修饰符。
  • 闭包返回单个 Double,Swift Charts 会在 X-Z 平面上自动采样并构造连续曲面。
  • 把任意数学表达式塞进闭包就能得到曲面图,无需手动构造网格。

要把曲面叠在数据上做拟合,可以先训练 MLLinearRegressor,再把它包装成同样的 (Double, Double) -> Double 闭包(06:19):

let linearRegression = LinearRegression(
  penguins,
  x: \.flipperLength,
  y: \.weight,
  z: \.beakLength
)

Chart3D {
  ForEach(penguins) { penguin in
    PointMark(
      x: .value("Flipper Length", penguin.flipperLength),
      y: .value("Weight", penguin.weight),
      z: .value("Beak Length", penguin.beakLength)
    )
    .foregroundStyle(by: .value("Species", penguin.species))
  }

  SurfacePlot(x: "Flipper Length", y: "Weight", z: "Beak Length") { flipperLength, beakLength in
    linearRegression(flipperLength, beakLength)
  }
  .foregroundStyle(.gray)
}

关键点:

  • Chart3D 内部支持 ForEach 与多个 mark 混合,散点和曲面共存。
  • LinearRegression 通过 callAsFunction 把模型预测包成普通闭包,喂给 SurfacePlot
  • 曲面用 .foregroundStyle(.gray) 与彩色散点形成对比,避免遮挡数据。

视角和投影通过 Chart3DPose 控制(08:09),azimuth 控制水平旋转、inclination 控制俯仰:

@State var pose = Chart3DPose(
  azimuth: .degrees(20),
  inclination: .degrees(7)
)

Chart3D(penguins) { penguin in
  // ...
}
.chart3DPose($pose)
.chart3DCameraProjection(.perspective)

关键点:

  • Chart3DPose 既能取预设值(.default.front),也能自定义角度。
  • chart3DPose($pose) 接受 Binding,方便随交互更新。
  • chart3DCameraProjection.orthographic(默认,无远近差异,适合从侧面退化成 2D)和 .perspective(远小近大,沉浸感更强)两种。

SurfacePlot 的着色还有专门的语义化样式(09:3809:47):.foregroundStyle(.heightBased) 按曲面高度上色、.foregroundStyle(.normalBased) 按曲面法线方向上色,也可以传 LinearGradientEllipticalGradient


核心启发

  • 做什么:用 Chart3D 把多个 2D 散点图合并为一张可旋转图。

    • 为什么值得做:用户原本要在脑中拼接多张 2D 图,3D 旋转可以直接看到聚类的真实形状,对生物、物理、传感器这类天然三维的数据尤其值。
    • 怎么开始:把现有 Chart 改成 Chart3D,给 PointMarkz,给三个轴都设 domain 和归一化的 range(如 -0.5...0.5),先看默认 pose,再用 chart3DPose 微调到典型数据集的最佳视角。
  • 做什么:用 SurfacePlot 把模型预测可视化为连续曲面。

    • 为什么值得做:线性回归、神经网络、物理仿真的输出都是 (Double, Double) -> Double 形式,曲面图能让用户直观看到模型与数据的偏差。
    • 怎么开始:把模型包装为 callAsFunction(_ x: Double, _ z: Double) -> Double,丢给 SurfacePlot 闭包,再用 .foregroundStyle(.gray) 与彩色 PointMark 一起叠在 Chart3D 里。
  • 做什么:在 visionOS 应用中优先考虑 3D 图表。

    • 为什么值得做:Vision Pro 是天然的空间显示设备,2D 图在空间里反而显得平。Swift Charts 3D 在 Vision Pro 上原生支持手势旋转。
    • 怎么开始:在 visionOS 的 SwiftUI 视图里直接用 Chart3D,把投影设为 .perspective,让远近关系帮助用户感知深度。
  • 做什么:根据使用场景选择 .orthographic.perspective

    • 为什么值得做:正交投影方便横向对比尺寸(远近一样大),透视投影提供深度线索。混用会让用户困惑。
    • 怎么开始:仪表盘、需要从侧面退化成 2D 阅读的图选 .orthographic;需要沉浸感、强调空间关系的展示选 .perspective,通过 chart3DCameraProjection 切换。

关联 Session

评论

GitHub Issues · utterances