SwiftUIDataVisualization

2.0.2

Statistical, financial and live charts for SwiftUI on iOS and macOS, built on Swift Charts.
muhittincamdali/SwiftUI-Data-Visualization

What's New

SwiftUI-Data-Visualization 2.0.2

2026-10-20T02:20:58Z

Fixed

  • SwiftUIDataVisualization.version reads 2.0.2. The 2.0.1 release still reported 2.0.0;
    a released tag cannot change, so the corrected value ships in this release.

Documentation

  • The README, its translations, the Accessible Charts article and the AudioGraphDescriptor
    documentation no longer say TreemapChart draws with Canvas; it draws its tiles with plain
    views. RadarChart and LiveTickerChart do draw with Canvas.
  • The installation instructions start from 2.0.2.

SwiftUI Data Visualization: Statistical, financial and live charts for SwiftUI on iOS and macOS, built on Swift Charts.

English · Türkçe · 简体中文 · 日本語 · 한국어 · Español · Deutsch · Français · Português (Brasil) · Русский

CI status Latest version: v2.0.2 Supported platforms: iOS 17+ · macOS 14+ Language version: Swift 6.2+ License: MIT

Get started Documentation Examples

A Swift package of chart views for SwiftUI on iOS 17+ and macOS 14+, built on Swift Charts. It covers the charts that Swift Charts leaves to you — time series with tens of thousands of points, histograms, box plots, trend lines, candlesticks, waterfalls, heatmaps, radar charts, treemaps and live tickers — and makes the analysis behind them public, so you can use it in a Chart of your own. The package has no dependencies.

Highlights

  • 10 chart views — TimeSeriesChart, HistogramChart, BoxPlotChart, ScatterChart, CandlestickChart, WaterfallChart, HeatmapChart, RadarChart, TreemapChart and LiveTickerChart. Each takes plain value types and needs only a frame.
  • Large and live data — TimeSeriesChart downsamples each series to 1,000 points with Largest-Triangle-Three-Buckets, so 50,000 readings per series stay smooth; it can scroll through a time window and report the selected date. LiveSeries keeps the latest values in a ring buffer for LiveTickerChart.
  • The analysis is public — Histogram (Sturges, Freedman–Diaconis, fixed width or count), BoxPlotSummary (Tukey whiskers), Statistics, LinearRegression, MovingAverage, NiceScale axis ticks, TreemapLayout (squarified), Candle.aggregate and Downsampling.
  • Safe with any input — values that are not finite and dates outside the years 1 to 4001 are left out, finite values beyond ±1e307 are drawn at that limit, ranges near Double.greatestFiniteMagnitude and subnormal ranges stay finite, and tick counts, bins, buffers, scroll windows and exports have upper limits. A dedicated test suite feeds nan, infinity, Int.max, extreme ranges and extreme dates to the analysis types and the charts.
  • Accessible — the Swift Charts based views get VoiceOver navigation and Audio Graphs, with readable labels for bins, boxes, candles and steps; the views drawn without Swift Charts (radar, treemap, ticker) attach an AudioGraphDescriptor. A color-blind safe palette is built in.
  • Tested — 188 Swift Testing tests in 33 suites, run on macOS and an iPhone simulator, including off-screen renders of every README sample in light and dark mode.
  • Usage samples that compile — every sample below comes from the example app, which CI builds for iOS and macOS.

What's included

Area Types
Time series TimeSeriesChart, TimeSeries, TimeSeriesPoint, MovingAverage, Downsampling
Statistics HistogramChart, Histogram, BoxPlotChart, BoxPlotSummary, ScatterChart, LinearRegression, Statistics
Finance CandlestickChart, Candle, WaterfallChart, WaterfallStep
Grids and shapes HeatmapChart, HeatmapCell, RadarChart, RadarGeometry, TreemapChart, TreemapLayout
Live data LiveTickerChart, LiveSeries, RingBuffer
Shared ChartPalette and .chartPalette(_:), NiceScale, ChartExport, AudioGraphDescriptor

Quick start

1. Add the package

In Xcode, choose File › Add Package Dependencies… and enter the repository URL:

https://github.com/muhittincamdali/SwiftUI-Data-Visualization.git

Paste it into the search field at the top right of the window. Leave Dependency Rule at Up to Next Major Version from 2.0.2, then click Add Package. In the next window, choose your app target next to SwiftUIDataVisualization and click Add Package again.

2. Import the module

import SwiftUIDataVisualization

Put this line at the top of the Swift file where you draw a chart, below import SwiftUI.

3. Use it

In a new Xcode app (File › New › Project… › App), replace everything in ContentView.swift with:

import SwiftUI
import SwiftUIDataVisualization

struct ContentView: View {
    let steps = TimeSeries(
        name: "Steps",
        values: [4_200, 8_100, 6_900, 10_400, 7_800, 12_100, 9_300],
        start: .now.addingTimeInterval(-6 * 86_400),
        interval: 86_400
    )

    var body: some View {
        TimeSeriesChart(steps)
            .frame(height: 240)
            .padding()
    }
}

Press Run (⌘R). The app shows a blue line of the last seven days; touch the line (or hover over it on the Mac) to see the date and the number of steps.

Installation

Swift Package Manager

In Xcode, choose File › Add Package Dependencies… and enter the repository URL:

https://github.com/muhittincamdali/SwiftUI-Data-Visualization.git

Or add it to the dependencies in your Package.swift:

dependencies: [
    .package(url: "https://github.com/muhittincamdali/SwiftUI-Data-Visualization.git", from: "2.0.2")
],
targets: [
    .target(name: "YourApp", dependencies: [
        .product(name: "SwiftUIDataVisualization", package: "SwiftUI-Data-Visualization")
    ])
]

Version 2.0.0 (October 2026) is the first release of the charts described here. The earlier release, 1.0.0, had a different API that did not build with current Xcode versions; 2.0.0 also brings in the work of SwiftUI-Charts-Pro, which is being archived. CHANGELOG.md lists the changes, including the breaking ones.

Usage

The samples below are copied from Examples/ChartGallery/ChartGallery/ReadmeSamples.swift. The example app and the test suite both compile that file.

Time series

struct ServerLoad: View {
    // 50,000 readings per server, one every 30 seconds; the chart draws 1,000 of each.
    let servers = [readings(name: "api-1", phase: 0), readings(name: "api-2", phase: 1)]

    static func readings(name: String, phase: Double) -> TimeSeries {
        let values = (0..<50_000).map { index -> Double in
            let wave = 25 * sin(Double(index) / 3_000 + phase)
            let noise = Double(index % 97) / 40
            return 40 + wave + noise
        }
        return TimeSeries(name: name, values: values, start: Date(timeIntervalSinceReferenceDate: 800_000_000), interval: 30)
    }

    var body: some View {
        TimeSeriesChart(servers, movingAverageWindow: 2_000)
            .frame(height: 240)
            .padding()
    }
}

The dashed lines are the moving averages over 2,000 readings. Pass style: .area to fill below the lines.

Scrolling, selection and export

struct ScrollingHistory: View {
    @State private var selected: Date?
    @State private var exported: Data?
    let load = ServerLoad.readings(name: "api-1", phase: 0)

    var chart: some View {
        // Two days at a time out of 17; the chart opens at the newest data.
        TimeSeriesChart(load, selection: $selected, visibleDuration: 2 * 86_400)
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text(selected.map { "Selected \($0.formatted(date: .abbreviated, time: .shortened))" } ?? "Touch the chart")
                .font(.headline)
            chart
                .frame(height: 220)
            Button("Export PNG") {
                exported = ChartExport.pngData(of: chart, size: CGSize(width: 600, height: 300))
            }
            if let exported {
                Text("\(exported.count) bytes")
            }
        }
        .padding()
    }
}

Statistics

struct LatencyReport: View {
    let ios: [Double] = [112, 98, 130, 101, 145, 120, 99, 180, 104, 117, 125, 133, 230, 108, 121]
    let android: [Double] = [140, 151, 128, 160, 149, 171, 138, 155, 162, 147, 158, 133, 166, 152]

    var body: some View {
        VStack(spacing: 24) {
            HistogramChart(values: ios + android, binning: .width(25), valueLabel: "Latency (ms)")
                .frame(height: 180)
            BoxPlotChart([
                .init(name: "iOS", values: ios),
                .init(name: "Android", values: android),
            ], valueLabel: "Latency (ms)")
            .frame(height: 200)
        }
        .padding()
    }
}

Without binning, the histogram chooses the bin width with Sturges' or the Freedman–Diaconis rule, whichever gives more bins. The box plot marks values beyond 1.5 interquartile ranges as outliers.

Trend line

struct HeightWeight: View {
    let people: [PlotPoint] = [
        .init(x: 158, y: 54), .init(x: 163, y: 60), .init(x: 167, y: 59), .init(x: 171, y: 68),
        .init(x: 174, y: 70), .init(x: 178, y: 74), .init(x: 181, y: 77), .init(x: 186, y: 85),
    ]

    var body: some View {
        ScatterChart(people, xLabel: "Height (cm)", yLabel: "Weight (kg)", showsTrendLine: true)
            .frame(height: 220)
            .padding()
    }
}

Finance

struct PriceAndProfit: View {
    // Trades every 5 minutes for three days, grouped into one candle per 6 hours.
    let candles = Candle.aggregate(trades, interval: 6 * 3_600)

    static let trades = (0..<864).map { index -> TimeSeriesPoint in
        let date = Date(timeIntervalSinceReferenceDate: 800_000_000 + Double(index) * 300)
        let price = 100 + 6 * sin(Double(index) / 40) + Double(index % 13) / 4
        return TimeSeriesPoint(date: date, value: price)
    }

    var body: some View {
        VStack(spacing: 24) {
            CandlestickChart(candles)
                .frame(height: 200)
            WaterfallChart([
                WaterfallStep("Revenue", 420),
                WaterfallStep("Costs", -260),
                WaterfallStep("Tax", -40),
                .total("Profit"),
            ])
            .frame(height: 200)
        }
        .padding()
    }
}

Heatmap

struct CommitActivity: View {
    let days = ["Mon", "Tue", "Wed", "Thu", "Fri"]
    let hours = ["9", "11", "13", "15", "17"]

    var cells: [HeatmapCell] {
        days.enumerated().flatMap { day, dayName in
            hours.enumerated().map { hour, hourName in
                HeatmapCell(column: hourName, row: dayName, value: Double((day * 7 + hour * 3) % 11))
            }
        }
    }

    var body: some View {
        HeatmapChart(cells, valueLabel: "Commits", showsValues: true)
            .frame(height: 220)
            .padding()
    }
}

Radar and treemap

struct ProfileAndStorage: View {
    var body: some View {
        VStack(spacing: 24) {
            RadarChart(
                axes: ["Speed", "Power", "Range", "Comfort", "Price"],
                series: [
                    .init(name: "Model A", values: [8, 6, 7, 9, 5]),
                    .init(name: "Model B", values: [6, 9, 5, 6, 8]),
                ],
                maximum: 10
            )
            .frame(height: 260)
            TreemapChart([
                TreemapItem("Photos", value: 48),
                TreemapItem("Apps", value: 31),
                TreemapItem("Music", value: 12),
                TreemapItem("Mail", value: 6),
                TreemapItem("Notes", value: 3),
            ])
            .frame(height: 200)
        }
        .padding()
    }
}

Live data

struct LiveLoad: View {
    @State private var load = LiveSeries(capacity: 120)

    var body: some View {
        LiveTickerChart(values: load.values, title: "CPU load")
            .frame(height: 80)
            .padding()
            .task {
                var tick = 0.0
                while !Task.isCancelled {
                    tick += 1
                    load.append(50 + 30 * sin(tick / 8)) // read the real value here
                    try? await Task.sleep(for: .milliseconds(100))
                }
            }
    }
}

Colors

struct ColorblindSafeCharts: View {
    var body: some View {
        LatencyReport()
            .chartPalette(.colorblindSafe)
    }
}

Every chart below the modifier uses the palette. The default, .ocean, is the blue palette in the screenshots; build your own with ChartPalette(series:positive:negative:neutral:gradient:).

Demo

Light Dark
Time series, light Time series, dark
Statistics, light Statistics, dark
Trend line, light Trend line, dark
Finance, light Finance, dark
Heatmap, light Heatmap, dark
Radar and treemap, light Radar and treemap, dark

The images are rendered on the iOS simulator from the Usage samples by Tests/SwiftUIDataVisualizationTests/RenderTests.swift and scripts/generate-screenshots.sh.

The time series and live data screens of the example app, captured on an iPhone simulator by scripts/capture-example-screens.sh:

Time series Live data
Time series screen, light Live data screen, light
Time series screen, dark Live data screen, dark

To try the charts yourself, open ChartGallery.xcodeproj in Examples/ChartGallery with Xcode, choose an iPhone simulator or My Mac, and press Run. The app has one screen per sample above; Examples/README.md has the details.

Documentation

Reference documentation and articles:

  • API reference (DocC, published to GitHub Pages from master)
  • Build it locally in Xcode with Product › Build Documentation, or from the command line:
xcodebuild docbuild -scheme SwiftUIDataVisualization -destination 'generic/platform=macOS'

Guides

Requirements

Platform Minimum version
iOS 17
macOS 14
Toolchain Swift 6.2 · Xcode 26+

CI builds with the default Xcode of GitHub's macos-26 runner image. The package is developed with Xcode 27.

Architecture

  • Sources/SwiftUIDataVisualization/Charts/ holds one file per chart view. Seven of them are Swift Charts Charts; RadarChart, TreemapChart and LiveTickerChart draw with Canvas or plain views, because Swift Charts has no polar or treemap coordinates and a ticker redrawn at display rate is cheaper in one Canvas pass.
  • Analysis/, Layout/ and Data/ hold the value types and functions the charts use (Histogram, BoxPlotSummary, LinearRegression, NiceScale, TreemapLayout, Candle, …). They have no view code and are Sendable, so they can run off the main actor.
  • The preparation each chart does before drawing (downsampling, whisker domains, candle body widths, waterfall bars, radar maximum) lives in nonisolated static functions that the tests call directly.
  • Streaming/ has RingBuffer and LiveSeries, the only reference type, an @Observable class on the main actor. Style/ChartPalette.swift passes colors through the environment.
  • CI runs swift test on macOS, xcodebuild test on an iPhone simulator, builds the example app for iOS and macOS, and publishes the DocC documentation.

Run the tests locally:

swift test                                    # macOS
xcodebuild test -scheme SwiftUIDataVisualization \
  -destination 'platform=iOS Simulator,name=iPhone 17'

Roadmap

  • Add the package to the Swift Package Index; .spi.yml is already in the repository.
  • Pinch to zoom on the time axis of TimeSeriesChart.
  • Test and enable visionOS, which Swift Charts supports.

Contributing

Contributions are welcome. Read the contributing guide and the code of conduct before opening a pull request. Report security issues privately as described in the security policy.

License

Released under the MIT license. See LICENSE.

Description

  • Swift Tools 6.2.0
View More Packages from this Author

Dependencies

  • None
Last updated: Sun Nov 01 2026 19:38:38 GMT-1000 (Hawaii-Aleutian Standard Time)