| name | swift-charts |
| description | Implement, review, or improve data visualizations using Swift Charts. Use when building bar, line, area, point, pie, donut, or iOS 26 3D charts; when adding chart selection, scrolling, annotations, axes, scales, legends, or foregroundStyle grouping; when plotting functions with BarPlot, LinePlot, AreaPlot, PointPlot, Chart3D, or SurfacePlot; or when creating heat maps, Gantt charts, grouped bars, sparklines, threshold lines, or spatial visualizations. |
Swift Charts
Build data visualizations with Swift Charts targeting iOS 26+. Compose marks
inside Chart or Chart3D, configure axes and scales with view modifiers, and
use vectorized plots or 3D plots when the data calls for them.
See references/charts-patterns.md for extended patterns, 3D charts, accessibility, and theming guidance.
Contents
Workflow
1. Build a new chart
- Define data as an
Identifiable struct or use id: key path.
- Choose mark type(s):
BarMark, LineMark, PointMark, AreaMark,
RuleMark, RectangleMark, SectorMark, or SurfacePlot.
- Wrap 2D marks in
Chart; use Chart3D only for real spatial or surface data.
- Encode visual channels:
.foregroundStyle(by:), .symbol(by:), .lineStyle(by:).
- Configure axes with
.chartXAxis / .chartYAxis.
- Set scale domains with
.chartXScale(domain:) / .chartYScale(domain:).
- Add selection, scrolling, or annotations as needed.
- For 1000+ 2D data points, use vectorized plots (
BarPlot, LinePlot, etc.).
2. Review existing chart code
Run through the Review Checklist at the end of this file.
Chart Container
Chart(sales) { item in
BarMark(x: .value("Month", item.month), y: .value("Revenue", item.revenue))
}
Chart {
ForEach(seriesA) { item in
LineMark(x: .value("Date", item.date), y: .value("Value", item.value))
.foregroundStyle(.blue)
}
RuleMark(y: .value("Target", 500))
.foregroundStyle(.red)
}
Chart(data, id: \.category) { item in
BarMark(x: .value("Category", item.category), y: .value("Count", item.count))
}
Mark Types
BarMark (iOS 16+)
BarMark(x: .value("Month", item.month), y: .value("Sales", item.sales))
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))
LineMark(x: .value("Date", item.date), y: .value("Temp", item.temp))
.foregroundStyle(by: .value("City", item.city))
.interpolationMethod(.catmullRom)
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+)
Use SectorMark for strictly positive values; filter, aggregate, or explain zero/negative values outside the pie or donut.
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))
}
Axis Customization
.chartXAxis(.hidden)
.chartYAxis(.hidden)
.chartXAxis {
AxisMarks(values: .stride(by: .month)) { value in
AxisGridLine()
AxisTick()
AxisValueLabel(format: .dateTime.month(.abbreviated))
}
}
.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)
Scale Configuration
.chartYScale(domain: 0...100)
.chartYScale(domain: .automatic(includesZero: true))
.chartYScale(domain: 1...10000, type: .log)
.chartXScale(domain: ["Mon", "Tue", "Wed", "Thu"])
Foreground Style and Encoding
BarMark(...).foregroundStyle(.blue)
BarMark(...).foregroundStyle(by: .value("Category", item.category))
AreaMark(...).foregroundStyle(
.linearGradient(colors: [.blue, .cyan], startPoint: .bottom, endPoint: .top)
)
Selection (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)
Scrollable Charts (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)
.chartScrollPosition(initialX: latestDate)
.chartScrollTargetBehavior(
.valueAligned(matching: DateComponents(hour: 0), majorAlignment: .page)
)
Annotations
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") }
Legend
.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)
}
}
}
Vectorized Plots (iOS 18+)
Use for large datasets (1000+ points). Accept entire collections or functions.
Chart {
BarPlot(sales, x: .value("Month", \.month), y: .value("Revenue", \.revenue))
.foregroundStyle(\.barColor)
}
Chart {
LinePlot(x: "x", y: "y", domain: -5...5) { x in sin(x) }
}
Chart {
LinePlot(x: "x", y: "y", t: "t", domain: 0...(2 * .pi)) { t in
(x: cos(t), y: sin(t))
}
}
Apply KeyPath-based modifiers before simple-value modifiers:
BarPlot(data, x: .value("X", \.x), y: .value("Y", \.y))
.foregroundStyle(\.color)
.opacity(0.8)
3D Charts (iOS 26+)
Use Chart3D for spatial data or bivariate surfaces, not as a decorative
replacement for ordinary 2D categorical or time-series charts. Chart3D
accepts SurfacePlot plus 3D initializers of PointMark, RuleMark, and
RectangleMark.
@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)
Common Mistakes
1. Missing series parameter for multi-line charts
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. Too many SectorMark slices
Chart(twentyCategories, id: \.name) { item in
SectorMark(angle: .value("Value", item.value))
}
Chart(groupedData, id: \.name) { item in
SectorMark(angle: .value("Value", item.value))
.foregroundStyle(by: .value("Category", item.name))
}
3. Missing scale domain when zero-baseline matters
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. Static foregroundStyle overriding data encoding
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. Individual marks for 10,000+ data points
Chart(largeDataset) { item in
PointMark(x: .value("X", item.x), y: .value("Y", item.y))
}
Chart {
PointPlot(largeDataset, x: .value("X", \.x), y: .value("Y", \.y))
}
6. Fixed chart height breaking Dynamic Type
Chart(data) { ... }
.frame(height: 200)
Chart(data) { ... }
.frame(minHeight: 200, maxHeight: 400)
7. KeyPath modifier after value modifier on vectorized plots
BarPlot(data, x: .value("X", \.x), y: .value("Y", \.y))
.opacity(0.8)
.foregroundStyle(\.color)
BarPlot(data, x: .value("X", \.x), y: .value("Y", \.y))
.foregroundStyle(\.color)
.opacity(0.8)
8. Missing accessibility labels
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. Treating angle selection as category selection
chartAngleSelection(value:) binds the selected plottable angle value. For
pie and donut charts, map that numeric value through cumulative sector ranges
before comparing it to a category label.
Review Checklist
References