All Articles
CategorySwiftUI
Reading Time
10 min read
Published
2026-10-15
Word Count
1,721words

Grab a coffee — this one is a deep dive!

SwiftUI-Data-Visualization: statistics, finance and live charts on top of Swift Charts

Summary

Why SwiftUI-Data-Visualization 2.0 builds on Swift Charts, keeps its analysis public, and clamps finite values that made Swift Charts stop the app.

  • SwiftUI-Data-Visualization is a Swift package of chart views for SwiftUI on iOS 17+ and macOS 14+.
  • Build on Swift Charts, not a custom renderer.
  • These samples are not only documentation.
  • The package's history is visible in the repository: opened in August 2025, a 1.0.0 release in February 2026, a reduction to placeholder code in June 2026, and the rebuild as 2.0.0 in October 2026.
SwiftUI-Data-Visualization: statistics, finance and live charts on top of Swift Charts

SwiftUI-Data-Visualization is a Swift package of chart views for SwiftUI on iOS 17+ and macOS 14+.

Version 2.0.0 rebuilt it on Swift Charts, and 2.0.1 followed the same night with fixes for inputs

that the first release did not handle. This article covers what the package is for, the

decisions behind it, the bugs found right after the release, and what it does not do yet.

Table of Contents


The problem

Swift Charts gives you marks: LineMark, BarMark, PointMark, RectangleMark. Turning raw data

into the right marks is left to you. A histogram needs bin edges. A box plot needs quartiles and

whiskers. A time series with 50,000 readings per series needs to be reduced before it is drawn. A

treemap needs a layout algorithm, and a radar chart needs polar coordinates, which Swift Charts

does not have.

There is a second, less visible problem: the data. Charts usually draw data the app did not

create — sensor readings, prices, imported files. A nan, an infinity, a date thousands of years

away or a value near Double.greatestFiniteMagnitude should not take the app down.

The package covers both. It ships ten chart views — TimeSeriesChart, HistogramChart,

BoxPlotChart, ScatterChart, CandlestickChart, WaterfallChart, HeatmapChart, RadarChart,

TreemapChart and LiveTickerChart — and makes the analysis behind them public, so the same

Histogram, BoxPlotSummary or Downsampling can be used in a Chart of your own. It has no

dependencies.

Design decisions and alternatives

Build on Swift Charts, not a custom renderer. The 1.0.0 release had its own LineChart,

BarChart and PieChart views, and its sources did not build with current Xcode versions. 2.0.0

removed that API: for plain line, bar, area and pie charts, Swift Charts' own marks are the answer,

and they come with axes, animation and VoiceOver support. Drawing everything with Canvas would

give full control, but every one of those features would have to be rebuilt. The cost of the

choice is the floor of iOS 17 and macOS 14, which the selection and scrolling APIs need.

Canvas only where Swift Charts has no answer. Seven of the ten views 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. Those three lose the accessibility Swift Charts provides for free, so each attaches

an AudioGraphDescriptor, an AXChartDescriptorRepresentable that describes the data to

VoiceOver's Audio Graphs.

Analysis separate from views. Analysis/, Layout/ and Data/ contain no view code and their

types 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 — lives in

nonisolated static functions that the tests call directly, without building a view.

Known algorithms, named in the source. NiceScale uses Paul Heckbert's "nice numbers" method,

so ticks fall on 1, 2 or 5 × 10ⁿ. Quantiles follow Hyndman and Fan's type 7, the same definition as

Excel's PERCENTILE.INC and NumPy's default, so results match the tools people check them

against. TreemapLayout is the squarified algorithm of Bruls, Huizing and van Wijk, which keeps

rectangles close to square; slice-and-dice is simpler but produces thin strips. TimeSeriesChart

reduces each series to 1,000 points with Largest-Triangle-Three-Buckets. Taking every n-th point

would be simpler, but it can skip a spike; LTTB keeps the first and last points and, from each

bucket, the point that forms the largest triangle with its neighbours, so the shape survives.

Total functions instead of thrown errors. Values that are not finite are left out, empty

statistics return nil instead of a fake zero, and limits such as tick counts, bin counts, the

memory a ring buffer reserves up front and scroll windows have upper bounds. The alternative, throws or precondition,

would either burden every call site or crash the app over one bad reading.

Live data with a value type and one observable class. RingBuffer is a struct with O(1)

appends. LiveSeries, the only reference type in the package, wraps it as an @Observable class on

the main actor, so LiveTickerChart updates through SwiftUI observation instead of Combine.

Colors through the environment. .chartPalette(_:) sets the palette for every chart below it.

The default is .ocean; .colorblindSafe uses the Okabe–Ito colors.

What broke: finite values and same-date candles

The 2.0.0 documentation said the charts accept any input. The tests covered nan, infinity and

extreme dates — an earlier fix in the same rebuild (7009887) had limited chart dates to

Date.distantPast...Date.distantFuture, because Swift Charts trapped when a date axis reached about

1e19 seconds. Finite but extreme values had not been tested.

They turned out to matter. Commit 9a67e99 records the failure: Swift Charts stopped the process

with "Double value cannot be converted to Int" when a numeric axis spanned more than about 1.6e308

or reached below about -1.5e308. A single -1.6e308 in a time series, or the points 1.7e308 and

-1 in a scatter chart, was enough. Swift Charts rounds the axis outward, the bound overflows and

is converted to Int. Seven charts passed such values to their marks unchanged.

The fix adds an internal ChartValues.supported range of -1e307...1e307, eight times narrower

than the first range that failed. Each chart clamps the value it hands to a mark; readouts, printed

heatmap values and VoiceOver labels keep the original value. The new "Extreme inputs: values" suite

renders all ten charts with seven value sets, and without the clamping it stops on the same fatal

error.

The same pull request fixed a second bug, in CandlestickChart.

Candle.id is the candle's date, and the chart passed its candles to Chart(_:) by that id. Swift

Charts draws one mark per id, so of two candles with the same date only the first was drawn, with

no warning. A render test showed it plainly: the chart with candles A and B on the same day was

pixel for pixel the chart with A alone. Commit 6b0c0b1 identifies candles by position instead:

swift
1// By position, not by `Candle.id`: candles that share a date would share an id, and
2// Swift Charts draws only one mark per id.
3Chart(Array(valid.enumerated()), id: \.offset) { _, candle in

Candle.id stays the date, so the public API did not change. The new test,

candlesThatShareADateAreAllDrawn, renders two same-day candles next to a third and checks that

removing either of them changes the image.

Both fixes, along with a NiceScale fix for spans near 1e308 and a VoiceOver label that read

"1 outliers", went out in pull request #18 as 2.0.1, less than an hour after 2.0.0. The lesson is

in the history: 2.0.0's list of handled inputs was incomplete because one class of input (finite

but huge) had not been tested at all. A render test that checks whether the image actually changes

catches bugs that a "the view builds" test does not.

Usage

Add the package with Swift Package Manager:

swift
1dependencies: [
2 .package(url: "https://github.com/muhittincamdali/SwiftUI-Data-Visualization.git", from: "2.0.0")
3],
4targets: [
5 .target(name: "YourApp", dependencies: [
6 .product(name: "SwiftUIDataVisualization", package: "SwiftUI-Data-Visualization")
7 ])
8]

The quick start from the README is a complete ContentView.swift:

swift
1import SwiftUI
2import SwiftUIDataVisualization
3 
4struct ContentView: View {
5 let steps = TimeSeries(
6 name: "Steps",
7 values: [4_200, 8_100, 6_900, 10_400, 7_800, 12_100, 9_300],
8 start: .now.addingTimeInterval(-6 * 86_400),
9 interval: 86_400
10 )
11 
12 var body: some View {
13 TimeSeriesChart(steps)
14 .frame(height: 240)
15 .padding()
16 }
17}

A statistics report with a histogram and a box plot:

swift
1struct LatencyReport: View {
2 let ios: [Double] = [112, 98, 130, 101, 145, 120, 99, 180, 104, 117, 125, 133, 230, 108, 121]
3 let android: [Double] = [140, 151, 128, 160, 149, 171, 138, 155, 162, 147, 158, 133, 166, 152]
4 
5 var body: some View {
6 VStack(spacing: 24) {
7 HistogramChart(values: ios + android, binning: .width(25), valueLabel: "Latency (ms)")
8 .frame(height: 180)
9 BoxPlotChart([
10 .init(name: "iOS", values: ios),
11 .init(name: "Android", values: android),
12 ], valueLabel: "Latency (ms)")
13 .frame(height: 200)
14 }
15 .padding()
16 }
17}

Without binning, the histogram picks Sturges' or the Freedman–Diaconis rule, whichever gives more

bins, and the box plot marks values beyond 1.5 interquartile ranges as outliers.

These samples are not only documentation. The README copies them from

Examples/ChartGallery/ChartGallery/ReadmeSamples.swift, which the ChartGallery example app

compiles, and the test target compiles the same file through a symlink and renders every sample in

light and dark mode. The screenshots in the README come from those renders. At tag v2.0.1, `swift

test` runs 188 tests in 33 suites; CI runs them on macOS and on an iPhone simulator.

Limits

  • Platforms: iOS 17+ and macOS 14+ only. visionOS is on the roadmap but not tested or enabled;

watchOS and tvOS are not supported.

  • Toolchain: Swift 6.2 tools and Xcode 26 or later. CI builds with the default Xcode of

GitHub's macos-26 runner image; other Xcode versions have not been tried.

  • The value and date limits are measured, not specified. -1e307...1e307 and the date range

come from Swift Charts' current behaviour. A future OS could change it; the extreme-input suites

would then fail and show it.

  • Clamping is visible. Values beyond ±1e307 are drawn at that limit, so two such values cannot

be told apart on screen, although readouts and VoiceOver keep the real value.

  • No benchmarks. The 1,000-point default and the scroll-window limit have reasons, but the

package has no benchmark suite; the old Benchmarks/ folder reported numbers no benchmark

produced and was removed in the rebuild.

  • Charts left out: Sankey, chord, violin, parallel coordinates and word cloud from

SwiftUI-Charts-Pro were not carried over. Each needs its own layout or estimation algorithm with

tests at the level of TreemapLayout and Histogram. Gauge, bubble, funnel, Gantt and sunburst

were left out because SwiftUI's Gauge or a few Swift Charts marks already cover them.

  • No pinch to zoom on the time axis yet; it is on the roadmap, along with listing the package

on the Swift Package Index.

The package's history is visible in the repository: opened in August 2025, a 1.0.0 release in

February 2026, a reduction to placeholder code in June 2026, and the rebuild as 2.0.0 in October 2026. The CHANGELOG states plainly that 1.0.0 did not build with current Xcode versions.

Source code: github.com/muhittincamdali/SwiftUI-Data-Visualization

Tags

#swiftui#swift-charts#data-visualization#statistics#accessibility#swift-package
Muhittin Çamdalı

Muhittin Çamdalı

Senior Mobile Engineer

Senior Mobile Engineer with 12+ years of experience. I build mobile products with iOS, Flutter and React Native, working full-stack and AI-native.

iOS Development News

Weekly Swift tips, SwiftUI tricks and iOS best practices. No spam, only valuable content.

Your subscription starts when you open the link in the confirmation email and press “Confirm my subscription”. The newsletter keeps open/click statistics; you can unsubscribe anytime with one click. Privacy

Share