# SwiftUI Charts Reference ## Table of Contents - [Overview](#overview) - [Availability](#availability) - [Core APIs](#core-apis) - [Chart Types](#chart-types) - [Axis Tweaks](#axis-tweaks) - [Selection APIs](#selection-apis) - [Annotations](#annotations) - [ChartProxy and Custom Touch Handling](#chartproxy-and-custom-touch-handling) - [Modifier Scope](#modifier-scope) - [Styling and Visual Channels](#styling-and-visual-channels) - [Composing Multiple Marks](#composing-multiple-marks) - [Animating Chart Data](#animating-chart-data) - [Best Practices](#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 the standard 2D chart APIs, axis customization, built-in selection APIs, annotations, and custom touch handling. ## Availability 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+ - Data-driven plot types such as `BarPlot` and `LinePlot` require iOS 18+ - Chart3D and Z-axis APIs exist on iOS 26+; this reference is primarily about 2D `Chart`, with a dedicated Chart3D section below ```swift if #available(iOS 17, *) { // Selection, SectorMark, scrollable axes } else { // Base Chart, axes, scales, and core marks } ``` ## Core APIs ### Import the Framework Always check that the file imports `Charts` before using `Chart`, `Chart3D`, `BarMark`, `SectorMark`, or `ChartProxy`. ```swift import SwiftUI import Charts ``` If chart types are unresolved, the first thing to verify is that `Charts` is imported in that file. ### Chart Container `Chart` is the root view. Add one or more marks inside it. ```swift 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. ```swift 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: ```swift 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. ```swift LineMark( x: .value("Day", entry.date), y: .value("Steps", entry.count) ) ``` ## Chart Types ### BarMark ```swift BarMark( x: .value("Product", product.name), y: .value("Units", product.units) ) ``` Stacking via `MarkStackingMethod`: `.standard`, `.normalized`, `.center`, `.unstacked`. ### LineMark ```swift 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 ```swift 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: ```swift AreaMark( x: .value("Day", sample.day), yStart: .value("Low", sample.low), yEnd: .value("High", sample.high) ) ``` ### PointMark ```swift PointMark( x: .value("Time", measurement.time), y: .value("Value", measurement.value) ) ``` ### RectangleMark ```swift RectangleMark( xStart: .value("Start Day", cell.startDay), xEnd: .value("End Day", cell.endDay), yStart: .value("Low", cell.low), yEnd: .value("High", cell.high) ) ``` ### RuleMark ```swift RuleMark(y: .value("Goal", 10_000)) .foregroundStyle(.red) ``` ### SectorMark Use `SectorMark` for pie and donut-style charts. `SectorMark` requires iOS 17 or later. ```swift 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 (iOS 18+) 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: ```swift 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 (iOS 26+) `Chart3D` is a separate API for 3D chart content. It supports 3D `PointMark`, `RectangleMark`, `RuleMark`, and `SurfacePlot`. ```swift 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: ```swift 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: - **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 **Always** gate `Chart3D` with `#available(iOS 26, *)` — it is not available on earlier OS versions. ## Axis Tweaks ### Axis Visibility and Labels Use `chartXAxis`, `chartYAxis`, `chartXAxisLabel`, and `chartYAxisLabel` on the `Chart` container. Axis visibility supports `.automatic`, `.visible`, and `.hidden`. ```swift 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. ```swift 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`, `.inset` - `position`: `.automatic`, `.leading`, `.trailing`, `.top`, `.bottom` - `values`: `.automatic`, `.automatic(desiredCount:)`, `.stride(by:)`, `.stride(by:count:)`, or an explicit array ### Axis Components Within `AxisMarks`, combine the built-in axis components as needed: ```swift AxisGridLine() AxisTick() AxisValueLabel() ``` `AxisValueLabel` can be tuned for dense axes: ```swift 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. ```swift 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: ```swift .chartXScale(domain: startDate...endDate) ``` ### Scrollable Axes (iOS 17+) For larger datasets, make the plot area scroll and control the visible domain. ```swift @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. ```swift @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. ```swift @State private var selectedWeeks: ClosedRange? 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. ```swift @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. ```swift 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`. ## ChartProxy and Custom Touch Handling Use `chartOverlay`/`chartBackground` (iOS 16+) or `chartGesture` (iOS 17+) with `ChartProxy` when built-in selection modifiers are not enough. ```swift .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 values - `position(forX:)`, `position(forY:)`, and `position(for:)` for placing custom overlays or indicators - `selectXValue(at:)`, `selectYValue(at:)`, `selectXRange(from:to:)`, and `selectYRange(from:to:)` for driving built-in selection from custom gestures - `plotFrame` (iOS 17+) or `plotAreaFrame` (iOS 16) with `plotSize` for converting between gesture coordinates and the plot area `select*` ChartProxy selection methods and `chartGesture` are available on iOS 17+. ## Modifier Scope Apply chart-wide modifiers to the `Chart` container and mark-specific modifiers to the individual mark. ```swift 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. ```swift 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. ```swift .chartForegroundStyleScale([ "North": .blue, "South": .orange, "East": .green ]) ``` For dynamic data where not all series appear at every point, use the mapping overload: ```swift .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`. ```swift 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 ```swift .chartLegend(.visible) .chartLegend(.hidden) .chartLegend(position: .bottom, alignment: .center) ``` ## Composing Multiple Marks Combine different mark types inside the same `Chart` closure: ```swift // 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. ```swift 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 `Identifiable` models (or explicit `id:`) for stable chart data identity - Use `foregroundStyle(by:)` for categorical series to get automatic legends and accessibility - Use `RuleMark` for goals, thresholds, and selected-value indicators - Use explicit `AxisMarks(values:)` when automatic tick generation gets crowded - Use `chartXScale` and `chartYScale` when you need stable visual comparisons - Use `chartXSelection(range:)` or `chartYSelection(range:)` for brushed selection - Gate iOS 17+ APIs such as `SectorMark` and selection with `#available` ### Don't - Put chart-wide modifiers such as `chartXAxis` or `chartXSelection` on individual marks - Apply manual `.foregroundStyle(.color)` per mark for categorical data — use `foregroundStyle(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 `BarMark` and `AreaMark` allow 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 - Forget that range selection is available only for X and Y axes, not angle selection For chart accessibility (VoiceOver, Audio Graph, `AXChartDescriptorRepresentable`), fallback strategies, WWDC sessions, and a full summary checklist, see `charts-accessibility.md`.