swift-charts
npx machina-cli add skill dpearson2699/swift-ios-skills/swift-charts --openclawSwift Charts
Build data visualizations with Swift Charts targeting iOS 26+. Compose marks
inside a Chart container, configure axes and scales with view modifiers, and
use vectorized plots for large datasets.
See references/charts-patterns.md for extended patterns, accessibility, and
theming guidance.
Workflow
1. Build a new chart
- Define data as an
Identifiablestruct or useid:key path. - Choose mark type(s):
BarMark,LineMark,PointMark,AreaMark,RuleMark,RectangleMark, orSectorMark. - Wrap marks in a
Chartcontainer. - 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+ 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
// Data-driven init (single-series)
Chart(sales) { item in
BarMark(x: .value("Month", item.month), y: .value("Revenue", item.revenue))
}
// Content closure init (multi-series, mixed marks)
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)
}
// Custom ID key path
Chart(data, id: \.category) { item in
BarMark(x: .value("Category", item.category), y: .value("Count", item.count))
}
Mark Types
BarMark (iOS 16+)
// Vertical bar
BarMark(x: .value("Month", item.month), y: .value("Sales", item.sales))
// Stacked by category (automatic when same x maps to multiple bars)
BarMark(x: .value("Month", item.month), y: .value("Sales", item.sales))
.foregroundStyle(by: .value("Product", item.product))
// Horizontal bar
BarMark(x: .value("Sales", item.sales), y: .value("Month", item.month))
// Interval bar (Gantt chart)
BarMark(
xStart: .value("Start", item.start),
xEnd: .value("End", item.end),
y: .value("Task", item.task)
)
LineMark (iOS 16+)
// Single line
LineMark(x: .value("Date", item.date), y: .value("Price", item.price))
// Multi-series via foregroundStyle encoding
LineMark(x: .value("Date", item.date), y: .value("Temp", item.temp))
.foregroundStyle(by: .value("City", item.city))
.interpolationMethod(.catmullRom)
// Multi-series with explicit series parameter
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+)
// Stacked area
AreaMark(x: .value("Date", item.date), y: .value("Sales", item.sales))
.foregroundStyle(by: .value("Category", item.category))
// Range band
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+)
// Pie chart
Chart(data, id: \.name) { item in
SectorMark(angle: .value("Sales", item.sales))
.foregroundStyle(by: .value("Category", item.name))
}
// Donut chart
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
// Hide axes
.chartXAxis(.hidden)
.chartYAxis(.hidden)
// Custom axis content
.chartXAxis {
AxisMarks(values: .stride(by: .month)) { value in
AxisGridLine()
AxisTick()
AxisValueLabel(format: .dateTime.month(.abbreviated))
}
}
// Multiple AxisMarks compositions (different intervals for grid vs. labels)
.chartXAxis {
AxisMarks(values: .stride(by: .day)) { _ in AxisGridLine() }
AxisMarks(values: .stride(by: .week)) { _ in
AxisTick()
AxisValueLabel(format: .dateTime.week())
}
}
// Axis labels (titles)
.chartXAxisLabel("Time", position: .bottom, alignment: .center)
.chartYAxisLabel("Revenue ($)", position: .leading, alignment: .center)
Scale Configuration
.chartYScale(domain: 0...100) // Explicit numeric domain
.chartYScale(domain: .automatic(includesZero: true)) // Include zero
.chartYScale(domain: 1...10000, type: .log) // Logarithmic scale
.chartXScale(domain: ["Mon", "Tue", "Wed", "Thu"]) // Categorical ordering
Foreground Style and Encoding
BarMark(...).foregroundStyle(.blue) // Static color
BarMark(...).foregroundStyle(by: .value("Category", item.category)) // Data encoding
AreaMark(...).foregroundStyle( // Gradient
.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: String?
// Point selection
Chart(data) { item in
LineMark(x: .value("Date", item.date), y: .value("Value", item.value))
}
.chartXSelection(value: $selectedDate)
// Range selection
.chartXSelection(range: $selectedRange)
// Angular selection (pie/donut)
.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) // 7 days visible
.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)
}
// Overflow resolution
.annotation(
position: .top,
overflowResolution: .init(x: .fit(to: .chart), y: .padScale)
) { Text("Label") }
Legend
.chartLegend(.hidden) // Hide
.chartLegend(position: .bottom, alignment: .center, spacing: 10) // Position
.chartLegend(position: .bottom) { // Custom
HStack(spacing: 16) {
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.
// Data-driven
Chart {
BarPlot(sales, x: .value("Month", \.month), y: .value("Revenue", \.revenue))
.foregroundStyle(\.barColor)
}
// Function plotting: y = f(x)
Chart {
LinePlot(x: "x", y: "y", domain: -5...5) { x in sin(x) }
}
// Parametric: (x, y) = f(t)
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) // KeyPath first
.opacity(0.8) // Value modifier second
Common Mistakes
1. Using ObservableObject instead of @Observable
// WRONG
class ChartModel: ObservableObject {
@Published var data: [Sale] = []
}
struct ChartView: View {
@StateObject private var model = ChartModel()
}
// CORRECT
@Observable class ChartModel {
var data: [Sale] = []
}
struct ChartView: View {
@State private var model = ChartModel()
}
2. Missing series parameter for multi-line charts
// WRONG -- all points connect into one line
Chart {
ForEach(allCities) { item in
LineMark(x: .value("Date", item.date), y: .value("Temp", item.temp))
}
}
// CORRECT -- separate lines per city
Chart {
ForEach(allCities) { item in
LineMark(x: .value("Date", item.date), y: .value("Temp", item.temp))
.foregroundStyle(by: .value("City", item.city))
}
}
3. Too many SectorMark slices
// WRONG -- 20 tiny sectors are unreadable
Chart(twentyCategories, id: \.name) { item in
SectorMark(angle: .value("Value", item.value))
}
// CORRECT -- group into top 5 + "Other"
Chart(groupedData, id: \.name) { item in
SectorMark(angle: .value("Value", item.value))
.foregroundStyle(by: .value("Category", item.name))
}
4. Missing scale domain when zero-baseline matters
// WRONG -- axis starts at ~95; small changes look dramatic
Chart(data) {
LineMark(x: .value("Day", $0.day), y: .value("Score", $0.score))
}
// CORRECT -- explicit domain for honest representation
Chart(data) {
LineMark(x: .value("Day", $0.day), y: .value("Score", $0.score))
}
.chartYScale(domain: 0...100)
5. Static foregroundStyle overriding data encoding
// WRONG -- static color overrides by-value encoding
BarMark(x: .value("X", item.x), y: .value("Y", item.y))
.foregroundStyle(by: .value("Category", item.category))
.foregroundStyle(.blue)
// CORRECT -- use only the data encoding
BarMark(x: .value("X", item.x), y: .value("Y", item.y))
.foregroundStyle(by: .value("Category", item.category))
6. Individual marks for 10,000+ data points
// WRONG -- creates 10,000 mark views; slow
Chart(largeDataset) { item in
PointMark(x: .value("X", item.x), y: .value("Y", item.y))
}
// CORRECT -- vectorized plot (iOS 18+)
Chart {
PointPlot(largeDataset, x: .value("X", \.x), y: .value("Y", \.y))
}
7. Fixed chart height breaking Dynamic Type
// WRONG -- clips axis labels at large text sizes
Chart(data) { ... }
.frame(height: 200)
// CORRECT -- adaptive sizing
Chart(data) { ... }
.frame(minHeight: 200, maxHeight: 400)
8. KeyPath modifier after value modifier on vectorized plots
// WRONG -- compiler error
BarPlot(data, x: .value("X", \.x), y: .value("Y", \.y))
.opacity(0.8)
.foregroundStyle(\.color)
// CORRECT -- KeyPath modifiers first
BarPlot(data, x: .value("X", \.x), y: .value("Y", \.y))
.foregroundStyle(\.color)
.opacity(0.8)
9. Missing accessibility labels
// WRONG -- VoiceOver users get no context
Chart(data) {
BarMark(x: .value("Month", $0.month), y: .value("Sales", $0.sales))
}
// CORRECT -- add per-mark accessibility
Chart(data) { item in
BarMark(x: .value("Month", item.month), y: .value("Sales", item.sales))
.accessibilityLabel("\(item.month)")
.accessibilityValue("\(item.sales) units sold")
}
Review Checklist
- Data model uses
Identifiableor chart usesid:key path - Model uses
@Observablewith@State, notObservableObject - Mark type matches goal (bar=comparison, line=trend, sector=proportion)
- Multi-series lines use
series:parameter or.foregroundStyle(by:) - Axes configured with appropriate labels, ticks, and grid lines
- Scale domain set explicitly when zero-baseline matters
- Pie/donut limited to 5-7 sectors; small values grouped into "Other"
- Selection binding type matches axis data type (
Date?for date axis) - Scrollable charts set
.chartXVisibleDomain(length:)for viewport - Vectorized plots used for datasets exceeding 1000 points
- KeyPath modifiers applied before value modifiers on vectorized plots
- Accessibility labels added to marks for VoiceOver
- Chart tested with Dynamic Type and Dark Mode
- Legend visible and positioned, or intentionally hidden
- Ensure chart data model types are Sendable; update chart data on @MainActor
API Verification
Use the apple-docs MCP fetchAppleDocumentation with paths like
/documentation/Charts/BarMark, /documentation/Charts/Chart, or
/documentation/Charts/LinePlot to verify API details.
References
- Extended patterns:
references/charts-patterns.md - Apple docs: Swift Charts
- Apple docs: Creating a chart using Swift Charts
Source
git clone https://github.com/dpearson2699/swift-ios-skills/blob/main/skills/swift-charts/SKILL.mdView on GitHub Overview
Swift Charts lets you build data visualizations by composing marks inside a Chart container, with configurable axes, scales, and styling. It supports a variety of marks (BarMark, LineMark, PointMark, AreaMark, RuleMark, RectangleMark, SectorMark) and vectorized plots for large datasets, enabling both standard and specialized visuals like heat maps or Gantt charts.
How This Skill Works
Define your data as an Identifiable type, choose one or more marks, and wrap them in a Chart container. Encode data channels (.foregroundStyle, .symbol, .lineStyle), configure axes with .chartXAxis/.chartYAxis and set domains with .chartXScale/.chartYScale. For large datasets, prefer vectorized plots (BarPlot, LinePlot, AreaPlot, PointPlot).
When to Use It
- Build bar, line, area, point, or donut charts to visualize data.
- Add chart interactions like selection, scrolling, or annotations.
- Plot functions with vectorized BarPlot, LinePlot, AreaPlot, or PointPlot for large datasets.
- Fine-tune axes, scales, legends, and foregroundStyle grouping for clarity.
- Create specialized visuals such as heat maps, Gantt charts, stacked/grouped bars, sparklines, or threshold lines.
Quick Start
- Step 1: Define your data and wrap marks in a Chart container.
- Step 2: Choose marks (BarMark, LineMark, AreaMark, PointMark, etc.) and encode channels (foregroundStyle, symbol, lineStyle).
- Step 3: Configure axes and scales (chartXAxis/chartYAxis, chartXScale/chartYScale) and add interactions if needed.
Best Practices
- Use vectorized plots for 1000+ data points to improve performance.
- Choose between data-driven init (single-series) and content closure init (multi-series) based on data shape.
- Encode multi-series with .foregroundStyle(by:) and consider explicit series for clarity.
- Always configure axes with .chartXAxis/.chartYAxis and set domains with .chartXScale(domain:)/.chartYScale(domain:).
- Leverage interactions (selection, scrolling, annotations) and plan accessibility/theming patterns.
Example Use Cases
- Monthly revenue bar chart using BarMark to compare across categories.
- Temperature trend line chart showing multiple cities with distinct foreground styles.
- Gantt-style schedule using BarMark with xStart/xEnd to illustrate task durations.
- Stacked or grouped bar chart encoded by category using foregroundStyle to differentiate groups.
- Threshold line added with RuleMark to highlight targets or limits.