Swift Charts 向量化绘图与函数图像
Swift Charts: Vectorized and function plots
2024年6月10日
一句话判断
Swift Charts 今年加了两把利器:函数绘图(LinePlot/AreaPlot)让数学可视化开箱即用,向量化 Plot API 让大数据集渲染性能直接起飞。
这场 Session 讲了什么
Swift Charts 在 iOS 16 发布时定位是”用 SwiftUI 的方式画数据图表”。今年的更新把它推向了两个新方向。
第一个方向是跳出”数据点”的框架,直接绘制数学函数。LinePlot 和 AreaPlot 两个新 API 接受一个闭包,输入 x 返回 y,就能画出函数曲线。支持正态分布、参数方程、分段函数,Audio Graph 也能自动为函数图生成无障碍音频描述。
第二个方向是性能优化。新增的向量化 Plot API(PointPlot、RectanglePlot 等)用 KeyPath 一次性处理整个数据集合,不再需要 ForEach 逐个遍历。对于包含数千甚至数万个点的散点图、热力图,渲染效率提升明显。
两个方向看似不同,底层是同一个设计理念:把”整体”当作一个实体来处理,而不是逐个元素的循环。
值得深挖的点
函数绘图:不只是画曲线,是数学可视化的一整套方案
LinePlot 看起来简单——传一个 (Double) -> Double 的闭包就完事了。但它背后的细节值得说清楚。
Swift Charts 会自动对函数进行采样来推断显示范围(domain),你不需要手动指定 x/y 的边界。但你完全可以覆盖这个行为,通过 .scaleX 和 .scaleY 精确控制显示区间。更进一步,AreaPlot 可以限制自身的采样域(sampling domain),只绘制函数的某一段。
AreaPlot 还支持”两函数之间填充”——返回 (yStart, yEnd) 元组就行。这在展示置信区间、误差范围时非常实用。
参数方程用同一个 LinePlot API,但返回 (x, y) 而不是单个 y。分段函数在无定义的区间返回 .nan,Swift Charts 会自动断开。这些设计都说明 Apple 在 API 表面之下做了不少边界处理。
向量化 Plot API:KeyPath 驱动的批量渲染
Mark API(BarMark、PointMark 等)灵活但慢——每个数据点都要单独创建一个 Mark,各自应用修饰符。对于几千个点的散点图,这个开销不可忽视。
向量化 Plot API 换了一个思路:你给一个集合,用 KeyPath 指定 x、y、颜色、大小等属性,Swift Charts 在内部批量处理。Session 明确建议:把 computed property 转成 stored property,这样 Swift Charts 可以用固定内存偏移量访问数据,避免为每个点调用 getter。
性能建议还包括:如果你的数据有几种不同的样式分组,先按样式排序再传入。Swift Charts 内部需要为每次样式切换付出开销,排序可以减少切换次数。
选择标准很清晰:少量数据点 + 需要单独定制样式 → Mark API;大量数据点 + 整体统一样式 → 向量化 Plot API。
代码片段
绘制正态分布曲线叠加在直方图上
Chart {
ForEach(histogramData) { point in
BarMark(x: .value("密度", point.density), y: .value("概率", point.probability))
}
// 函数绘图:直接画正态分布曲线
LinePlot(x: "密度", y: "概率") { x in
normalDistribution(x: x, mean: mean, std: std)
}
.foregroundStyle(.red)
}
场景:在数据直方图上叠加理论分布曲线进行对比。坑:LinePlot 和 BarMark 默认颜色可能相同,记得用 foregroundStyle 区分。
向量化散点图
// 数据结构用 stored property 而非 computed property
struct DataPoint: Identifiable {
let id: UUID
let x: Double // 预计算的投影坐标
let y: Double // 预计算的投影坐标
let capacity: Double
let axisType: AxisType
}
Chart(dataPoints) { _ in
PointPlot(x: .value("经度", \.x), y: .value("纬度", \.y))
.symbolSize(.value("容量", \.capacity))
.foregroundStyle(.value("类型", \.axisType))
}
场景:数千个太阳能面板安装点在地图上的散点可视化。坑:一定要用 stored property 传 KeyPath,computed property 会严重拖慢渲染。
参数方程绘图
Chart {
LinePlot(x: "x", y: "y") { t in
let x = cos(t) * (1 + 0.5 * cos(5 * t))
let y = sin(t) * (1 + 0.5 * cos(5 * t))
return (x, y)
}
}
场景:绘制花形参数曲线等数学图案。坑:确保 t 的范围覆盖完整曲线,否则图形会不完整。
最佳实践
已有项目迁移:如果你用 ForEach + MarkMark 画大数据图表(超过 100 个点),优先迁移到向量化 Plot API。改动不大——主要是把 ForEach 循环换成 Plot 的 KeyPath 语法,同时确保数据模型的属性是 stored property。函数绘图是新能力,可以在需要叠加理论曲线或数学可视化时引入。
新项目起步:从第一天就建立”数据规模决定 API 选择”的意识。100 个点以内用 Mark API 没问题,超过这个量级就考虑向量化 Plot。如果你的 App 涉及任何科学计算或数据分析,函数绘图能力值得立刻上手。
还有什么值得关注
- 函数图支持 Audio Graph,VoiceOver 用户可以用音频感知函数曲线的走势——无障碍支持做得比大多数第三方图表库都好。
- AreaPlot 的
(yStart, yEnd)元组返回方式让”两条曲线之间填充”变得极其简单,不用再手动构造上下界数据。 - 所有 Mark 类型都有对应的向量化 Plot 变体(RectanglePlot、LinePlot 等),不只是 PointPlot。