📦 deps(thirdparty): update snapshots
This commit is contained in:
@@ -0,0 +1,602 @@
|
||||
# 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<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.
|
||||
|
||||
```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`.
|
||||
Reference in New Issue
Block a user