Agents swift-charts

Building data visualizations in SwiftUI using Apple's Swift Charts framework

install
source · Clone the upstream repo
git clone https://github.com/aRustyDev/agents
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/aRustyDev/agents "$T" && mkdir -p ~/.claude/skills && cp -r "$T/content/plugins/frontend/swiftui-dev/skills/swift-charts" ~/.claude/skills/arustydev-agents-swift-charts && rm -rf "$T"
manifest: content/plugins/frontend/swiftui-dev/skills/swift-charts/SKILL.md
source content

Swift Charts

Purpose

Building data visualizations in SwiftUI using Apple's Swift Charts framework.

Basic Charts

Line Chart

import Charts
import SwiftUI

struct TimeSeriesView: View {
    let data: [DataPoint]

    var body: some View {
        Chart(data) { point in
            LineMark(
                x: .value("Date", point.date),
                y: .value("Value", point.value)
            )
        }
        .chartXAxis {
            AxisMarks(values: .stride(by: .month)) { value in
                AxisValueLabel(format: .dateTime.month(.abbreviated))
            }
        }
    }
}

Bar Chart

struct CategoryChart: View {
    let sales: [CategorySales]

    var body: some View {
        Chart(sales) { item in
            BarMark(
                x: .value("Category", item.category),
                y: .value("Sales", item.amount)
            )
            .foregroundStyle(by: .value("Category", item.category))
        }
        .chartLegend(.hidden)
    }
}

Scatter Plot

struct ScatterView: View {
    let points: [Observation]

    var body: some View {
        Chart(points) { point in
            PointMark(
                x: .value("X", point.x),
                y: .value("Y", point.y)
            )
            .foregroundStyle(by: .value("Cluster", point.cluster))
            .symbolSize(point.weight * 10)
        }
    }
}

Area Chart

struct StackedAreaChart: View {
    let data: [CategoryTimeSeries]

    var body: some View {
        Chart(data) { series in
            ForEach(series.values) { point in
                AreaMark(
                    x: .value("Date", point.date),
                    y: .value("Value", point.value)
                )
            }
            .foregroundStyle(by: .value("Category", series.category))
        }
        .chartForegroundStyleScale([
            "Sales": .blue,
            "Expenses": .red,
            "Profit": .green
        ])
    }
}

Advanced Marks

Combined Marks

struct CombinedChart: View {
    let data: [DataPoint]

    var body: some View {
        Chart(data) { point in
            // Line for trend
            LineMark(
                x: .value("Date", point.date),
                y: .value("Value", point.value)
            )
            .foregroundStyle(.blue)

            // Points for individual values
            PointMark(
                x: .value("Date", point.date),
                y: .value("Value", point.value)
            )
            .foregroundStyle(.blue)

            // Rule for average
            RuleMark(y: .value("Average", average))
                .foregroundStyle(.red.opacity(0.5))
                .lineStyle(StrokeStyle(dash: [5, 5]))
        }
    }

    var average: Double {
        data.map(\.value).reduce(0, +) / Double(data.count)
    }
}

Range Marks

struct RangeChart: View {
    let data: [RangeData]

    var body: some View {
        Chart(data) { item in
            // Show range
            RectangleMark(
                x: .value("Category", item.category),
                yStart: .value("Min", item.min),
                yEnd: .value("Max", item.max)
            )
            .opacity(0.3)

            // Show median
            PointMark(
                x: .value("Category", item.category),
                y: .value("Median", item.median)
            )
        }
    }
}

Interactivity

Selection

struct InteractiveChart: View {
    let data: [DataPoint]
    @State private var selectedDate: Date?

    var body: some View {
        Chart(data) { point in
            LineMark(
                x: .value("Date", point.date),
                y: .value("Value", point.value)
            )

            if let selected = selectedDate,
               let point = data.first(where: { Calendar.current.isDate($0.date, inSameDayAs: selected) }) {
                RuleMark(x: .value("Selected", point.date))
                    .foregroundStyle(.gray.opacity(0.3))

                PointMark(
                    x: .value("Date", point.date),
                    y: .value("Value", point.value)
                )
                .foregroundStyle(.red)
                .symbolSize(100)
            }
        }
        .chartXSelection(value: $selectedDate)
        .chartOverlay { proxy in
            GeometryReader { geometry in
                if let selected = selectedDate,
                   let point = data.first(where: { Calendar.current.isDate($0.date, inSameDayAs: selected) }),
                   let position = proxy.position(forX: point.date) {
                    VStack {
                        Text(point.date, format: .dateTime.month().day())
                        Text(point.value, format: .number)
                            .bold()
                    }
                    .padding(8)
                    .background(.ultraThinMaterial)
                    .cornerRadius(8)
                    .position(x: position, y: 20)
                }
            }
        }
    }
}

Scrolling Charts

struct ScrollableChart: View {
    let data: [DataPoint]
    @State private var scrollPosition: Date = .now

    var body: some View {
        Chart(data) { point in
            LineMark(
                x: .value("Date", point.date),
                y: .value("Value", point.value)
            )
        }
        .chartScrollableAxes(.horizontal)
        .chartXVisibleDomain(length: 3600 * 24 * 30) // 30 days
        .chartScrollPosition(x: $scrollPosition)
    }
}

Styling

Custom Colors

Chart(data) { point in
    BarMark(
        x: .value("Category", point.category),
        y: .value("Value", point.value)
    )
    .foregroundStyle(by: .value("Category", point.category))
}
.chartForegroundStyleScale([
    "Category A": Color.blue,
    "Category B": Color.green,
    "Category C": Color.orange
])

Axis Customization

Chart(data) { point in
    LineMark(
        x: .value("Date", point.date),
        y: .value("Value", point.value)
    )
}
.chartXAxis {
    AxisMarks(preset: .aligned) { value in
        AxisGridLine()
        AxisTick()
        AxisValueLabel(format: .dateTime.month())
    }
}
.chartYAxis {
    AxisMarks(position: .leading) { value in
        AxisGridLine()
        AxisValueLabel {
            if let intValue = value.as(Int.self) {
                Text("$\(intValue)")
            }
        }
    }
}

Plot Area Styling

Chart(data) { point in
    LineMark(
        x: .value("X", point.x),
        y: .value("Y", point.y)
    )
}
.chartPlotStyle { plotArea in
    plotArea
        .background(.gray.opacity(0.1))
        .border(.gray, width: 1)
}

Performance (Large Datasets)

Vectorized Plots (WWDC24)

// For datasets > 1000 points
struct LargeDatasetChart: View {
    let points: [Point]  // Thousands of points

    var body: some View {
        Chart {
            PointPlot(points, x: \.x, y: \.y)
                .foregroundStyle(.blue)
        }
    }
}

Data Aggregation

struct AggregatedChart: View {
    let rawData: [DataPoint]

    var aggregatedData: [DataPoint] {
        // Aggregate to reasonable number of points
        Dictionary(grouping: rawData) { point in
            Calendar.current.startOfDay(for: point.date)
        }
        .map { date, points in
            DataPoint(
                date: date,
                value: points.map(\.value).reduce(0, +) / Double(points.count)
            )
        }
        .sorted(by: { $0.date < $1.date })
    }

    var body: some View {
        Chart(aggregatedData) { point in
            LineMark(
                x: .value("Date", point.date),
                y: .value("Value", point.value)
            )
        }
    }
}

Chart3D (macOS 26+ / iOS 20+)

import Charts

struct Scatter3DView: View {
    let points: [Point3D]

    var body: some View {
        Chart3D(points) { point in
            PointMark3D(
                x: .value("X", point.x),
                y: .value("Y", point.y),
                z: .value("Z", point.z)
            )
            .foregroundStyle(by: .value("Cluster", point.cluster))
        }
        .chart3DStyle(.interactive) // Allows rotation
    }
}

Common Patterns

Empty State

struct ChartWithEmptyState: View {
    let data: [DataPoint]

    var body: some View {
        if data.isEmpty {
            ContentUnavailableView(
                "No Data",
                systemImage: "chart.bar.xaxis",
                description: Text("Add some data to see the chart")
            )
        } else {
            Chart(data) { point in
                LineMark(
                    x: .value("Date", point.date),
                    y: .value("Value", point.value)
                )
            }
        }
    }
}

Loading State

struct ChartWithLoading: View {
    let data: [DataPoint]?
    let isLoading: Bool

    var body: some View {
        ZStack {
            if let data {
                Chart(data) { point in
                    LineMark(
                        x: .value("Date", point.date),
                        y: .value("Value", point.value)
                    )
                }
            }

            if isLoading {
                ProgressView()
            }
        }
    }
}

Related Skills

  • duckdb-swift: Data source for charts
  • swiftui-data-flow: Reactive chart updates
  • grape-graphs: Force-directed graph visualization