.agents/skills/swiftui-expert-skill/references/charts.md
Swift Charts is Apple's native charting framework for SwiftUI. Use Chart with one or more marks to build bar, line, area, point, rule, rectangle, and sector charts. This reference covers the standard 2D chart APIs, axis customization, built-in selection APIs, annotations, and custom touch handling.
Base Chart, custom axes, scales, and most marks require iOS 16 or later.
BarMark, LineMark, AreaMark, PointMark, RectangleMark, and RuleMark are available on iOS 16+SectorMark, built-in selection, and scrollable chart axes require iOS 17+BarPlot and LinePlot require iOS 18+Chart, with a dedicated Chart3D section belowif #available(iOS 17, *) {
// Selection, SectorMark, scrollable axes
} else {
// Base Chart, axes, scales, and core marks
}
Always check that the file imports Charts before using Chart, Chart3D, BarMark, SectorMark, or ChartProxy.
import SwiftUI
import Charts
If chart types are unresolved, the first thing to verify is that Charts is imported in that file.
Chart is the root view. Add one or more marks inside it.
Chart(sales) { item in
BarMark(
x: .value("Month", item.month),
y: .value("Revenue", item.revenue)
)
}
Prefer Identifiable models for chart data so identity stays stable as data changes.
struct SalesPoint: Identifiable {
let id: UUID
let month: String
let revenue: Double
}
If your model cannot conform to Identifiable, provide an explicit id key path:
Chart(sales, id: \.month) { item in
BarMark(
x: .value("Month", item.month),
y: .value("Revenue", item.revenue)
)
}
Use .value(_, _) to describe what each axis value means. Those labels are reused by axes, legends, and accessibility.
LineMark(
x: .value("Day", entry.date),
y: .value("Steps", entry.count)
)
BarMark(
x: .value("Product", product.name),
y: .value("Units", product.units)
)
Stacking via MarkStackingMethod: .standard, .normalized, .center, .unstacked.
LineMark(
x: .value("Day", day.date),
y: .value("Steps", day.count)
)
.interpolationMethod(.monotone)
Interpolation methods: .linear, .monotone, .cardinal, .catmullRom, .stepStart, .stepCenter, .stepEnd. Cardinal and Catmull-Rom accept optional tension/alpha parameters.
AreaMark(
x: .value("Hour", sample.hour),
y: .value("Temperature", sample.value),
stacking: .unstacked
)
Ranged areas use yStart/yEnd for bands like min/max or confidence intervals:
AreaMark(
x: .value("Day", sample.day),
yStart: .value("Low", sample.low),
yEnd: .value("High", sample.high)
)
PointMark(
x: .value("Time", measurement.time),
y: .value("Value", measurement.value)
)
RectangleMark(
xStart: .value("Start Day", cell.startDay),
xEnd: .value("End Day", cell.endDay),
yStart: .value("Low", cell.low),
yEnd: .value("High", cell.high)
)
RuleMark(y: .value("Goal", 10_000))
.foregroundStyle(.red)
Use SectorMark for pie and donut-style charts. SectorMark requires iOS 17 or later.
Chart(expenses) { expense in
SectorMark(
angle: .value("Amount", expense.amount),
innerRadius: .ratio(0.6),
angularInset: 2
)
.foregroundStyle(by: .value("Category", expense.category))
}
Use innerRadius to turn a pie chart into a donut chart, and angularInset to separate slices visually.
iOS 18 adds data-driven plot wrappers: AreaPlot, BarPlot, LinePlot, PointPlot, RectanglePlot, RulePlot, and SectorPlot.
LinePlot and AreaPlot also accept function closures for plotting mathematical functions without discrete data:
if #available(iOS 18, *) {
Chart {
LinePlot(x: "x", y: "sin(x)") { x in
sin(x)
}
}
.chartXScale(domain: -Double.pi ... Double.pi)
.chartYScale(domain: -1.5 ... 1.5)
}
Use plot types when you want a data-first API surface or need function plotting. The underlying chart families stay the same.
Chart3D is a separate API for 3D chart content. It supports 3D PointMark, RectangleMark, RuleMark, and SurfacePlot.
if #available(iOS 26, *) {
Chart3D(points) { point in
PointMark(
x: .value("X", point.x),
y: .value("Y", point.y),
z: .value("Z", point.z)
)
}
.chart3DPose(.front)
.chart3DCameraProjection(.perspective)
}
SurfacePlot visualizes mathematical surfaces by evaluating a two-variable function:
if #available(iOS 26, *) {
Chart3D {
SurfacePlot(x: "x", y: "height", z: "z") { x, z in
sin(x) * cos(z)
}
}
.chartXScale(domain: -Double.pi ... Double.pi)
.chartZScale(domain: -Double.pi ... Double.pi)
}
Camera and pose configuration:
.chart3DCameraProjection(.orthographic) (default, precise measurements) or .perspective (depth effect).chart3DPose(.default), .front, .back, .left, .right.chart3DPose(azimuth: .degrees(45), inclination: .degrees(30))Always gate Chart3D with #available(iOS 26, *) — it is not available on earlier OS versions.
Use chartXAxis, chartYAxis, chartXAxisLabel, and chartYAxisLabel on the Chart container.
Axis visibility supports .automatic, .visible, and .hidden.
Chart(data) { item in
BarMark(
x: .value("Month", item.month),
y: .value("Revenue", item.revenue)
)
}
.chartXAxis(.visible)
.chartYAxis(.hidden)
.chartXAxisLabel("Month")
.chartYAxisLabel("Revenue")
Use AxisMarks to control tick placement, labels, and grid lines.
Chart(steps) { day in
LineMark(
x: .value("Day", day.date),
y: .value("Steps", day.count)
)
}
.chartXAxis {
AxisMarks(
preset: .aligned,
position: .bottom,
values: .stride(by: .day)
) {
AxisGridLine()
AxisTick(length: .label)
AxisValueLabel(format: .dateTime.weekday(.abbreviated))
}
}
Useful AxisMarks inputs:
preset: .automatic, .extended, .aligned, .insetposition: .automatic, .leading, .trailing, .top, .bottomvalues: .automatic, .automatic(desiredCount:), .stride(by:), .stride(by:count:), or an explicit arrayWithin AxisMarks, combine the built-in axis components as needed:
AxisGridLine()
AxisTick()
AxisValueLabel()
AxisValueLabel can be tuned for dense axes:
AxisValueLabel(
collisionResolution: .greedy(minimumSpacing: 8),
orientation: .vertical
)
Label orientations: .automatic, .horizontal, .vertical, .verticalReversed.
Collision strategies: .automatic, .greedy, .greedy(priority:minimumSpacing:), .truncate, .disabled.
Use scales when you need explicit axis domains or plot area control.
Chart(data) { item in
LineMark(
x: .value("Index", item.index),
y: .value("Score", item.score)
)
}
.chartXScale(domain: 0...30)
.chartYScale(domain: 0...100)
.chartPlotStyle { plotArea in
plotArea
.background(.gray.opacity(0.08))
}
You can set one axis domain without forcing the other:
.chartXScale(domain: startDate...endDate)
For larger datasets, make the plot area scroll and control the visible domain.
@State private var scrollX = 7
Chart(data) { item in
BarMark(
x: .value("Day", item.day),
y: .value("Value", item.value)
)
}
.chartScrollableAxes(.horizontal)
.chartXVisibleDomain(length: 7)
.chartScrollPosition(x: $scrollX)
Use chartXSelection(value:) or chartYSelection(value:) for one selected value.
@State private var selectedDate: Date?
Chart(steps) { day in
LineMark(x: .value("Day", day.date), y: .value("Steps", day.count))
if let selectedDate {
RuleMark(x: .value("Selected Day", selectedDate))
.foregroundStyle(.secondary)
}
}
.chartXSelection(value: $selectedDate)
Use chartXSelection(range:) or chartYSelection(range:) for a dragged range. Bind to a ClosedRange whose bound type matches the plotted axis value.
@State private var selectedWeeks: ClosedRange<Int>?
Chart(weeks) { week in
BarMark(x: .value("Week", week.index), y: .value("Revenue", week.revenue))
}
.chartXSelection(range: $selectedWeeks)
value: bindings when only one point or axis value should be selected.range: bindings when users should brush a span (for zoom windows, comparisons, or grouped summaries).Use chartAngleSelection(value:) with SectorMark charts. No built-in range overload for angle selection.
@State private var selectedAmount: Double?
Chart(expenses) { expense in
SectorMark(angle: .value("Amount", expense.amount))
.foregroundStyle(by: .value("Category", expense.category))
}
.chartAngleSelection(value: $selectedAmount)
Important: Selection bindings return the plottable axis value, not the full data element. Map back to your model if you need the selected record.
Use annotation(position:) on a mark when you need labels, callouts, or highlighted values attached to the plotted content.
BarMark(
x: .value("Month", item.month),
y: .value("Revenue", item.revenue)
)
.annotation(position: .top) {
Text(item.revenue.formatted())
}
This is useful for selected values, thresholds, summaries, and direct labeling. Common positions include .overlay, .top, .bottom, .leading, and .trailing.
Use chartOverlay/chartBackground (iOS 16+) or chartGesture (iOS 17+) with ChartProxy when built-in selection modifiers are not enough.
.chartOverlay { proxy in
GeometryReader { geometry in
Rectangle().fill(.clear).contentShape(Rectangle())
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { value in
guard let plotFrame = proxy.plotFrame else { return } // iOS 16: use proxy.plotAreaFrame
let frame = geometry[plotFrame]
let x = value.location.x - frame.origin.x
guard x >= 0, x <= frame.size.width else { return }
selectedDate = proxy.value(atX: x, as: Date.self)
}
.onEnded { _ in selectedDate = nil }
)
}
}
Use proxy.plotFrame (iOS 17+) or proxy.plotAreaFrame (iOS 16) to get the plot area anchor.
ChartProxy gives you lower-level access to:
value(atX:as:), value(atY:as:), and value(at:as:) for converting gesture coordinates into chart valuesposition(forX:), position(forY:), and position(for:) for placing custom overlays or indicatorsselectXValue(at:), selectYValue(at:), selectXRange(from:to:), and selectYRange(from:to:) for driving built-in selection from custom gesturesplotFrame (iOS 17+) or plotAreaFrame (iOS 16) with plotSize for converting between gesture coordinates and the plot areaselect* ChartProxy selection methods and chartGesture are available on iOS 17+.
Apply chart-wide modifiers to the Chart container and mark-specific modifiers to the individual mark.
Chart(data) { item in
LineMark(
x: .value("Day", item.date),
y: .value("Value", item.value)
)
.interpolationMethod(.monotone) // Mark-level modifier
}
.chartXAxis { AxisMarks() } // Chart-level modifier
.chartYScale(domain: 0...100) // Chart-level modifier
.chartPlotStyle { $0.background(.thinMaterial) }
Use foregroundStyle(by: .value(...)) to color marks by a data property. Swift Charts generates a legend automatically.
Chart(sales) { item in
BarMark(
x: .value("Month", item.month),
y: .value("Revenue", item.revenue)
)
.foregroundStyle(by: .value("Region", item.region))
}
Avoid applying .foregroundStyle(.red) per mark for categorical data — this suppresses the automatic legend and breaks accessibility.
Use chartForegroundStyleScale to control the mapping from data values to colors.
.chartForegroundStyleScale([
"North": .blue,
"South": .orange,
"East": .green
])
For dynamic data where not all series appear at every point, use the mapping overload:
.chartForegroundStyleScale(domain: regions, mapping: { region in
colorForRegion(region)
})
Use symbol(by:) and symbolSize(by:) to encode additional data dimensions on PointMark and LineMark.
Chart(measurements) { item in
PointMark(
x: .value("Time", item.time),
y: .value("Value", item.value)
)
.foregroundStyle(by: .value("Category", item.category))
.symbol(by: .value("Category", item.category))
.symbolSize(by: .value("Weight", item.weight))
}
.chartLegend(.visible)
.chartLegend(.hidden)
.chartLegend(position: .bottom, alignment: .center)
Combine different mark types inside the same Chart closure:
// Line with points
LineMark(x: .value("Day", day.date), y: .value("Steps", day.count))
.interpolationMethod(.monotone)
PointMark(x: .value("Day", day.date), y: .value("Steps", day.count))
// Bars with threshold line
BarMark(x: .value("Month", item.month), y: .value("Revenue", item.revenue))
RuleMark(y: .value("Target", 10_000))
.foregroundStyle(.red)
.lineStyle(StrokeStyle(dash: [5, 3]))
Chart marks animate automatically when data identity is stable and changes are wrapped in an animation.
withAnimation(.easeInOut) {
chartData = updatedData
}
Always use Identifiable models (or explicit id:) so Swift Charts can match old and new data points and animate transitions between them.
.value(_, _) labels so axes and accessibility read clearlyIdentifiable models (or explicit id:) for stable chart data identityforegroundStyle(by:) for categorical series to get automatic legends and accessibilityRuleMark for goals, thresholds, and selected-value indicatorsAxisMarks(values:) when automatic tick generation gets crowdedchartXScale and chartYScale when you need stable visual comparisonschartXSelection(range:) or chartYSelection(range:) for brushed selectionSectorMark and selection with #availablechartXAxis or chartXSelection on individual marks.foregroundStyle(.color) per mark for categorical data — use foregroundStyle(by:) insteadBarMark and AreaMark allow itFor chart accessibility (VoiceOver, Audio Graph, AXChartDescriptorRepresentable), fallback strategies, WWDC sessions, and a full summary checklist, see charts-accessibility.md.