
Guide Swiftui Charts
- 178 installs
- 297 repo stars
- Updated August 4, 2026
- vabole/apple-skills
Build SwiftUI Charts views—bar, line, area, and rule marks—with axes, legends, accessibility labels, and live data binding for analytics dashboards on Apple platforms.
About
Covers SwiftUI Charts for iOS and macOS: mark types, axes, scales, legends, interactions, accessibility, and animated data updates so agents ship polished in-app analytics visuals without third-party chart libraries.
- Mark types: bar, line, point, area
- Axes, domains, and chart scales
- Legends, annotations, and interactions
- Accessibility and Dynamic Type
- Animating series and live updates
Guide Swiftui Charts by the numbers
- 178 all-time installs (skills.sh)
- +5 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #491 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/vabole/apple-skills --skill guide-swiftui-chartsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 178 |
|---|---|
| repo stars | ★ 297 |
| Last updated | August 4, 2026 |
| Repository | vabole/apple-skills ↗ |
What it does
Build SwiftUI Charts views—bar, line, area, and rule marks—with axes, legends, accessibility labels, and live data binding for analytics dashboards on Apple platforms.
Files
Guide Skill — This is an expert workflow/pattern guide, not API reference documentation.
Originally from AvdLee/SwiftUI-Agent-Skill by Antoine van der Lee and Omar Elsayed. MIT License.
Swift Charts
References
| Topic | Reference |
|---|---|
| Marks, axes, selection, styling, composition, Chart3D | references/charts.md |
| Accessibility, VoiceOver, Audio Graph, custom descriptors | references/charts-accessibility.md |
Swift Charts Accessibility and Resources
Table of Contents
---
Accessibility
Swift Charts provides built-in accessibility support. VoiceOver users get three rotor actions automatically:
- Describe Chart — overview of axes and data series
- Audio Graph — sonification where pitch represents data values
- Chart Detail — interactive mode for exploring individual data points
Meaningful Labels
Always use clear, descriptive strings in .value(_, _) calls. These labels are read by VoiceOver and used in the Audio Graph.
// Good — descriptive labels
LineMark(
x: .value("Date", entry.date),
y: .value("Daily Steps", entry.count)
)
// Bad — generic labels
LineMark(
x: .value("X", entry.date),
y: .value("Y", entry.count)
)Custom Audio Graphs
For advanced accessibility, conform your chart view to AXChartDescriptorRepresentable and implement makeChartDescriptor(). Attach it with .accessibilityChartDescriptor(self).
struct StepsChart: View, AXChartDescriptorRepresentable {
let steps: [DailySteps]
var body: some View {
Chart(steps) { day in
LineMark(x: .value("Date", day.date), y: .value("Steps", day.count))
}
.accessibilityChartDescriptor(self)
}
func makeChartDescriptor() -> AXChartDescriptor {
guard let first = steps.first, let last = steps.last else {
return AXChartDescriptor(title: "Daily Step Count", summary: nil,
xAxis: AXNumericDataAxisDescriptor(title: "Date", range: 0...1, gridlinePositions: []) { "\($0)" },
yAxis: AXNumericDataAxisDescriptor(title: "Steps", range: 0...1, gridlinePositions: []) { "\($0)" },
additionalAxes: [], series: [])
}
let xAxis = AXDateDataAxisDescriptor(
title: "Date", range: first.date...last.date, gridlinePositions: [])
let yAxis = AXNumericDataAxisDescriptor(
title: "Steps", range: 0...Double(steps.map(\.count).max() ?? 0),
gridlinePositions: []) { "\(Int($0)) steps" }
let series = AXDataSeriesDescriptor(
name: "Daily Steps", isContinuous: true,
dataPoints: steps.map { .init(x: $0.date, y: Double($0.count)) })
return AXChartDescriptor(title: "Daily Step Count", summary: nil,
xAxis: xAxis, yAxis: yAxis, additionalAxes: [], series: [series])
}
}Composite Example
A scrollable bar chart with range selection:
@State private var selectedRange: ClosedRange<Int>?
Chart(weeklyRevenue) { week in
BarMark(x: .value("Week", week.index), y: .value("Revenue", week.revenue))
.foregroundStyle(by: .value("Region", week.region))
}
.chartScrollableAxes(.horizontal)
.chartXVisibleDomain(length: 8)
.chartXSelection(range: $selectedRange)
.chartXAxis {
AxisMarks(values: .stride(by: 1)) {
AxisGridLine()
AxisValueLabel { Text("W\($0.as(Int.self) ?? 0)") }
}
}WWDC Sessions
- Hello Swift Charts (WWDC 2022) — introduction to the framework
- Swift Charts: Raise the bar (WWDC 2022) — marks, composition, customization
- Design an effective chart (WWDC 2022) — chart design principles
- Design app experiences with charts (WWDC 2022) — integrating charts into app UX
- Explore pie charts and interactivity in Swift Charts (WWDC 2023) — SectorMark, selection, scrolling
- Swift Charts: Vectorized and function plots (WWDC 2024) — LinePlot, AreaPlot, function plotting
- Bring Swift Charts to the third dimension (WWDC 2025) — Chart3D, SurfacePlot, 3D marks
Summary Checklist
- [ ]
import Chartsis present in files using chart types - [ ] Chart data models use
Identifiable(orChart(data, id:)is provided) - [ ] All chart families are represented with the correct mark type
- [ ] Axes use
AxisMarkswhen default ticks are too dense or unclear - [ ]
chartXScaleorchartYScaleis set when fixed domains matter - [ ] Chart-wide modifiers are applied to
Chart, not individual marks - [ ]
foregroundStyle(by:)used for categorical series (not manual per-mark colors) - [ ]
.value()labels are descriptive for VoiceOver and Audio Graph accessibility
SwiftUI Charts Reference
Table of Contents
- Overview
- Core APIs
- Chart Types
- Axis Tweaks
- Selection APIs
- Annotations
- ChartProxy and Custom Touch Handling
- Modifier Scope
- Styling and Visual Channels
- Composing Multiple Marks
- Animating Chart Data
- Best Practices
Overview
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 2D and 3D chart APIs, axis customization, selection, annotations, and custom touch handling.
Core APIs
Import the Framework
Always check that the file imports Charts before using Chart, Chart3D, BarMark, SectorMark, or ChartProxy.
import SwiftUI
import ChartsChart Container
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)
)
}Data Models Should Be Identifiable
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)
)
}Plottable Values
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)
)Chart Types
BarMark
BarMark(
x: .value("Product", product.name),
y: .value("Units", product.units)
)Stacking via MarkStackingMethod: .standard, .normalized, .center, .unstacked.
LineMark
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
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
PointMark(
x: .value("Time", measurement.time),
y: .value("Value", measurement.value)
)RectangleMark
RectangleMark(
xStart: .value("Start Day", cell.startDay),
xEnd: .value("End Day", cell.endDay),
yStart: .value("Low", cell.low),
yEnd: .value("High", cell.high)
)RuleMark
RuleMark(y: .value("Goal", 10_000))
.foregroundStyle(.red)SectorMark
Use SectorMark for pie and donut-style charts.
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.
Plot Types
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:
Chart {
LinePlot(x: "x", y: "sin(x)") { x in
sin(x)
}
}
.chartXScale(domain: -Double.pi ... Double.pi)
.chartYScale(domain: -1.5 ... 1.5)Chart3D
Chart3D is a separate API for 3D chart content. It supports 3D PointMark, RectangleMark, RuleMark, and SurfacePlot.
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:
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:
- Projection:
.chart3DCameraProjection(.orthographic)(default, precise measurements) or.perspective(depth effect) - Pose presets:
.chart3DPose(.default),.front,.back,.left,.right - Custom pose:
.chart3DPose(azimuth: .degrees(45), inclination: .degrees(30)) - On visionOS, Chart3D supports natural 3D interaction gestures for rotation and exploration
Axis Tweaks
Axis Visibility and Labels
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")Custom Axis Marks
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 array
Axis Components
Within 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.
Axis Domains and Plot Area Tweaks
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))
}Scrollable Axes
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)Selection APIs
Single-Value Selection
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)Range Selection
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)Choosing Single vs Range
- Use
value:bindings when only one point or axis value should be selected. - Use
range:bindings when users should brush a span (for zoom windows, comparisons, or grouped summaries).
Angle Selection
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.
Annotations
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())
}Common positions include .overlay, .top, .bottom, .leading, and .trailing.
ChartProxy and Custom Touch Handling
Use chartOverlay/chartBackground or chartGesture 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 }
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 }
)
}
}ChartProxy gives you lower-level access to:
value(atX:as:),value(atY:as:), andvalue(at:as:)for converting gesture coordinates into chart valuesposition(forX:),position(forY:), andposition(for:)for placing custom overlays or indicatorsselectXValue(at:),selectYValue(at:),selectXRange(from:to:), andselectYRange(from:to:)for driving built-in selection from custom gesturesplotFramewithplotSizefor converting between gesture coordinates and the plot area
Modifier Scope
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) }Styling and Visual Channels
Categorical Coloring
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.
Custom Color Scales
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)
})Symbol and Size Channels
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))
}Legend Control
.chartLegend(.visible)
.chartLegend(.hidden)
.chartLegend(position: .bottom, alignment: .center)Composing Multiple Marks
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]))Animating Chart Data
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.
Best Practices
Do
- Use semantic
.value(_, _)labels so axes and accessibility read clearly - Prefer
Identifiablemodels (or explicitid:) for stable chart data identity - Use
foregroundStyle(by:)for categorical series to get automatic legends and accessibility - Use
RuleMarkfor goals, thresholds, and selected-value indicators - Use explicit
AxisMarks(values:)when automatic tick generation gets crowded - Use
chartXScaleandchartYScalewhen you need stable visual comparisons - Use
chartXSelection(range:)orchartYSelection(range:)for brushed selection
Don't
- Put chart-wide modifiers such as
chartXAxisorchartXSelectionon individual marks - Apply manual
.foregroundStyle(.color)per mark for categorical data — useforegroundStyle(by:)instead - Rely on unstable identities when chart data can be inserted, removed, or reordered
- Use string values for naturally numeric or date-based axes unless you want categorical behavior
- Stack unrelated series by default just because
BarMarkandAreaMarkallow it - Force every tick label to display when collision handling or stride values would be clearer
- Assume selection returns a model object; it only returns the plottable axis value
For chart accessibility (VoiceOver, Audio Graph, AXChartDescriptorRepresentable), see charts-accessibility.md.