swift-charts

Implement, review, or improve data visualizations using Swift Charts. Use when building bar, line, area, point, pie, or donut charts; when adding chart selection, scrolling, or annotations; when plotting functions with vectorized BarPlot, LinePlot, AreaPlot, or PointPlot; when customizing axes, scales, legends, or foregroundStyle grouping; or when creating specialized visualizations like heat maps, Gantt charts, stacked/grouped bars, sparklines, or threshold lines.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "swift-charts" with this command: npx skills add dpearson2699/swift-ios-skills/dpearson2699-swift-ios-skills-swift-charts

Swift 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.

Contents

Workflow

1. Build a new chart

  1. Define data as an Identifiable struct or use id: key path.
  2. Choose mark type(s): BarMark, LineMark, PointMark, AreaMark, RuleMark, RectangleMark, or SectorMark.
  3. Wrap marks in a Chart container.
  4. Encode visual channels: .foregroundStyle(by:), .symbol(by:), .lineStyle(by:).
  5. Configure axes with .chartXAxis / .chartYAxis.
  6. Set scale domains with .chartXScale(domain:) / .chartYScale(domain:).
  7. Add selection, scrolling, or annotations as needed.
  8. 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 Identifiable or chart uses id: key path
  • Model uses @Observable with @State, not ObservableObject
  • 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

References

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

swiftui-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
General

swiftui-animation

No summary provided by upstream source.

Repository SourceNeeds Review
General

ios-accessibility

No summary provided by upstream source.

Repository SourceNeeds Review
General

swiftdata

No summary provided by upstream source.

Repository SourceNeeds Review