使用 Swift Charts 實作、審查或改善資料視覺化。適用於建立長條圖、折線圖、區域圖、散佈圖、圓餅圖、甜甜圈圖或 iOS 26 的 3D 圖表;新增圖表選取、捲動、註解、軸線、尺度、圖例或 foregroundStyle 分組;使用 BarPlot、LinePlot、AreaPlot、PointPlot、Chart3D 或 SurfacePlot 繪製函數;或建立熱圖、甘特圖、群組長條圖、走勢圖、閾值線或空間視覺化。
Swift Charts
使用 Swift Charts 建立資料視覺化,目標 iOS 26 以上。在 Chart 或 Chart3D 內組合標記,使用檢視修飾器設定軸線與尺度,並在資料需要時使用向量化繪圖或 3D 繪圖。
請參閱 references/charts-patterns.md 以取得進階模式、3D 圖表、無障礙與主題設定指引。
目錄
- 工作流程
- 圖表容器
- 標記類型
- 軸線自訂
- 尺度設定
- 前景樣式與編碼
- 選取 (iOS 17+)
- 可捲動圖表 (iOS 17+)
- 註解
- 圖例
- 向量化繪圖 (iOS 18+)
- 3D 圖表 (iOS 26+)
- 常見錯誤
- 審查檢查清單
- 參考資料
工作流程
1. 建立新圖表
- 將資料定義為
Identifiable結構體,或使用id:鍵路徑。 - 選擇標記類型:
BarMark、LineMark、PointMark、AreaMark、RuleMark、RectangleMark、SectorMark或SurfacePlot。 - 將 2D 標記包在
Chart中;僅在真正的空間或曲面資料時使用Chart3D。 - 編碼視覺通道:
.foregroundStyle(by:)、.symbol(by:)、.lineStyle(by:)。 - 使用
.chartXAxis/.chartYAxis設定軸線。 - 使用
.chartXScale(domain:)/.chartYScale(domain:)設定尺度範圍。 - 視需要新增選取、捲動或註解。
- 若 2D 資料點超過 1000 筆,使用向量化繪圖(
BarPlot、LinePlot等)。 - 呈現代表性的空白、典型、密集、大字型、高對比及 VoiceOver 狀態;若有選取或捲動功能,也一併測試。
- 若編碼、軸線、選取或無障礙檢查失敗,還原資料固定值,修正一層後重新執行相同矩陣,再新增裝飾。
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 以及 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. 多折線圖缺少 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. 固定圖表高度破壞 Dynamic Type
// 錯誤 -- 大字型時裁切軸線標籤
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
- [ ] 圖表已測試 Dynamic Type 與深色模式
- [ ] 圖例可見且已定位,或刻意隱藏
- [ ] 確保圖表資料模型型別為 Sendable;在 @MainActor 上更新圖表資料
參考資料
- 進階模式:references/charts-patterns.md
- Apple 文件:Swift Charts
- Apple 文件:使用 Swift Charts 建立圖表
- Apple 文件:Swift Charts 更新
- Apple 文件:Chart3D
- Apple 文件:SurfacePlot






