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
- Design decisions and alternatives
- What broke: finite values and same-date candles
- Usage
- Limits
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:
1// By position, not by `Candle.id`: candles that share a date would share an id, and2// Swift Charts draws only one mark per id.3Chart(Array(valid.enumerated()), id: \.offset) { _, candle inCandle.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:
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:
1import SwiftUI2import SwiftUIDataVisualization3 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_40010 )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:
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...1e307and 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
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

