SwiftUI-Data-Visualization, iOS 17+ ve macOS 14+ üzerinde SwiftUI için grafik view'ları sunan bir
Swift paketi. 2.0.0 sürümüyle Swift Charts üzerine yeniden kuruldu; 2.0.1 de aynı gece, ilk
sürümün ele almadığı girdiler için düzeltmelerle geldi. Bu yazı paketin ne işe yaradığını,
arkasındaki kararları, yayından hemen sonra bulunan hataları ve henüz yapmadığı şeyleri anlatıyor.
İçindekiler
- Problem
- Tasarım kararları ve alternatifler
- Ne bozuldu: sonlu değerler ve aynı tarihli mumlar
- Kullanım
- Sınırlar
Problem
Swift Charts size mark'ları veriyor: LineMark, BarMark, PointMark, RectangleMark. Ham veriyi
doğru mark'lara dönüştürmek ise size kalıyor. Histogram bin kenarlarına ihtiyaç duyar. Box plot
çeyrekleri ve whisker'ları ister. Seri başına 50.000 okuma içeren bir zaman serisinin çizilmeden
önce küçültülmesi gerekir. Treemap bir yerleşim algoritması ister; radar grafiği ise Swift
Charts'ta bulunmayan kutupsal koordinatlara ihtiyaç duyar.
Daha az görünen ikinci bir sorun da veri. Grafikler çoğu zaman uygulamanın kendi üretmediği veriyi
çizer: sensör okumaları, fiyatlar, içe aktarılan dosyalar. Bir nan, bir sonsuz değer, binlerce
yıl uzaktaki bir tarih ya da Double.greatestFiniteMagnitude civarında bir değer uygulamayı
çökertmemeli.
Paket ikisini de ele alıyor. On grafik view'ı içeriyor: TimeSeriesChart, HistogramChart,
BoxPlotChart, ScatterChart, CandlestickChart, WaterfallChart, HeatmapChart, RadarChart,
TreemapChart ve LiveTickerChart. Arkalarındaki analizi de public yapıyor; aynı Histogram,
BoxPlotSummary ya da Downsampling kendi Chart'ınızda kullanılabiliyor. Hiçbir bağımlılığı yok.
Tasarım kararları ve alternatifler
Özel bir çizim motoru değil, Swift Charts. 1.0.0 sürümünün kendi LineChart, BarChart ve
PieChart view'ları vardı ve kaynak kodu güncel Xcode sürümleriyle derlenmiyordu. 2.0.0 bu
API'yi kaldırdı: düz çizgi, bar, alan ve pasta grafikleri için Swift Charts'ın kendi mark'ları
yeterli; eksenler, animasyon ve VoiceOver desteği de onlarla birlikte geliyor. Her şeyi Canvas ile
çizmek tam kontrol sağlardı, ama bu özelliklerin hepsini yeniden yazmak gerekirdi. Bu seçimin
bedeli, seçim ve kaydırma API'lerinin gerektirdiği iOS 17 ve macOS 14 alt sınırı.
Canvas yalnız Swift Charts'ın karşılamadığı yerde. On view'ın yedisi Swift Charts Chart'ı.
RadarChart, TreemapChart ve LiveTickerChart ise Canvas ya da düz view'larla çiziyor; çünkü
Swift Charts'ta kutupsal ya da treemap koordinatları yok, ekran yenileme hızında güncellenen bir
ticker'ı tek bir Canvas geçişinde çizmek de daha ucuz. Bu üçü Swift Charts'ın hazır verdiği
erişilebilirliği kaybediyor; bu yüzden her biri, veriyi VoiceOver'ın Audio Graph'larına anlatan bir
AXChartDescriptorRepresentable olan AudioGraphDescriptor ekliyor.
Analiz view'lardan ayrı. Analysis/, Layout/ ve Data/ klasörlerinde view kodu yok ve
tipler Sendable; main actor dışında çalışabiliyorlar. Her grafiğin çizmeden önce yaptığı
hazırlık (downsampling, whisker aralıkları, mum gövdesi genişlikleri, waterfall çubukları)
nonisolated static fonksiyonlarda duruyor ve testler bunları view kurmadan doğrudan çağırıyor.
Bilinen algoritmalar, kaynakta adıyla. NiceScale, Paul Heckbert'in "nice numbers" yöntemini
kullanıyor; tick'ler 1, 2 ya da 5 × 10ⁿ değerlerine oturuyor. Quantile hesabı Hyndman ve Fan'ın
type 7 tanımını izliyor; bu, Excel'in PERCENTILE.INC fonksiyonu ve NumPy'nin varsayılanıyla aynı,
yani sonuçlar insanların karşılaştırdığı araçlarla tutuyor. TreemapLayout, Bruls, Huizing ve van
Wijk'in squarified algoritması; dikdörtgenleri kareye yakın tutuyor. Slice-and-dice daha basit ama
ince şeritler üretiyor. TimeSeriesChart her seriyi Largest-Triangle-Three-Buckets ile 1.000
noktaya indiriyor. Her n'inci noktayı almak daha basit olurdu ama ani bir tepeyi atlayabilir; LTTB
ilk ve son noktayı tutuyor, her kovadan da komşularıyla en büyük üçgeni oluşturan noktayı seçiyor,
böylece eğrinin şekli korunuyor.
Hata fırlatmak yerine her girdiye cevap veren fonksiyonlar. Sonlu olmayan değerler atlanıyor,
boş veride istatistik sahte bir sıfır yerine nil dönüyor; tick sayısı, bin sayısı, ring
buffer'ın önceden ayırdığı bellek ve kaydırma pencereleri gibi değerlerin üst sınırı var. Alternatif olan throws ya da
precondition, ya her çağrı noktasına yük bindirir ya da tek bir bozuk okuma yüzünden uygulamayı
çökertir.
Canlı veri için bir value type ve tek bir gözlemlenebilir sınıf. RingBuffer, O(1) eklemeli
bir struct. Paketteki tek reference type olan LiveSeries onu main actor üzerinde @Observable
bir sınıf olarak sarıyor; LiveTickerChart Combine yerine SwiftUI observation ile güncelleniyor.
Renkler environment üzerinden. .chartPalette(_:) altındaki tüm grafiklerin paletini
belirliyor. Varsayılan .ocean; .colorblindSafe ise Okabe–Ito renklerini kullanıyor.
Ne bozuldu: sonlu değerler ve aynı tarihli mumlar
2.0.0'ın dokümanı grafiklerin her girdiyi kabul ettiğini söylüyordu. Testler nan, sonsuz değer
ve aşırı tarihleri kapsıyordu; aynı yeniden kurulumdaki daha önceki bir düzeltme (7009887), tarih
ekseni yaklaşık 1e19 saniyeye ulaştığında Swift Charts süreci durdurduğu için grafik tarihlerini
Date.distantPast...Date.distantFuture aralığına sınırlamıştı. Sonlu ama aşırı büyük değerler ise
test edilmemişti.
Bunların önemli olduğu ortaya çıktı. 9a67e99 commit'i hatayı kaydediyor: sayısal bir eksen
yaklaşık 1.6e308'den geniş bir aralığa yayıldığında ya da yaklaşık -1.5e308'in altına indiğinde
Swift Charts süreci "Double value cannot be converted to Int" hatasıyla durduruyordu. Bir zaman
serisinde tek bir -1.6e308 ya da bir scatter grafiğinde 1.7e308 ile -1 noktaları yetiyordu.
Swift Charts ekseni dışa doğru yuvarlıyor, sınır taşıyor ve Int'e çevriliyor. Yedi grafik bu
değerleri mark'larına olduğu gibi veriyordu.
Düzeltme, internal bir ChartValues.supported aralığı ekliyor: -1e307...1e307; hata veren ilk
aralıktan sekiz kat dar. Her grafik mark'a verdiği değeri bu aralığın sınırına çekiyor; readout'lar,
heatmap'te yazılan değerler ve VoiceOver etiketleri orijinal değeri koruyor. Yeni
"Extreme inputs: values" süiti on grafiğin hepsini yedi değer kümesiyle çiziyor; sınıra çekme olmadan
aynı fatal error ile duruyor.
Aynı pull request ikinci bir hatayı da düzeltti, bu kez CandlestickChart'ta. Candle.id mumun
tarihiydi ve grafik mumları Chart(_:)'e bu id ile veriyordu. Swift Charts id başına bir mark
çiziyor; aynı tarihli iki mumdan yalnız ilki çiziliyordu ve hiçbir uyarı yoktu. Bir render testi
bunu açıkça gösterdi: aynı gün A ve B mumlarını içeren grafik, yalnız A'yı içeren grafikle piksel
piksel aynıydı. 6b0c0b1 commit'i mumları konumlarıyla tanımlıyor:
swift
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 tarih olarak kaldı, yani public API değişmedi. Yeni test,
candlesThatShareADateAreAllDrawn, aynı tarihli iki mumu üçüncü bir mumun yanında çiziyor ve
ikisinden birini kaldırmanın görüntüyü değiştirdiğini doğruluyor.
Bu iki düzeltme, 1e308 civarındaki aralıklar için bir NiceScale düzeltmesi ve "1 outliers" diye
okunan bir VoiceOver etiketiyle birlikte #18 numaralı pull request ile 2.0.1 olarak çıktı; 2.0.0'dan
bir saat bile geçmeden. Ders geçmişte duruyor: 2.0.0'ın ele aldığı girdiler listesi eksikti, çünkü
bir girdi sınıfı (sonlu ama çok büyük) hiç test edilmemişti. Görüntünün gerçekten değişip
değişmediğine bakan bir render testi, "view derleniyor" testinin yakalamadığı hataları yakalıyor.
Kullanım
Paketi Swift Package Manager ile ekleyin:
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]README'deki hızlı başlangıç, eksiksiz bir ContentView.swift:
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}Histogram ve box plot içeren bir istatistik raporu:
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}binning verilmezse histogram, Sturges ya da Freedman–Diaconis kuralından hangisi daha çok bin
veriyorsa onu seçiyor; box plot da çeyrekler açıklığının 1,5 katından daha uzaktaki değerleri aykırı değer
olarak işaretliyor.
Bu örnekler yalnızca doküman değil. README bunları Examples/ChartGallery/ChartGallery/ReadmeSamples.swift
dosyasından kopyalıyor; ChartGallery örnek uygulaması bu dosyayı derliyor, test target'ı da aynı
dosyayı bir symlink üzerinden derleyip her örneği açık ve koyu temada çiziyor. README'deki ekran
görüntüleri bu çizimlerden geliyor. v2.0.1 tag'inde swift test 33 süitte 188 test koşturuyor; CI
bunları macOS'ta ve bir iPhone simülatöründe çalıştırıyor.
Sınırlar
- Platformlar: yalnız iOS 17+ ve macOS 14+. visionOS yol haritasında, ama test edilmedi ve
açılmadı; watchOS ve tvOS desteklenmiyor.
- Araç zinciri: Swift 6.2 tools ve Xcode 26 ya da üstü. CI, GitHub'ın
macos-26runner
imajındaki varsayılan Xcode ile derliyor; başka Xcode sürümleri denenmedi.
- Değer ve tarih sınırları ölçüm, şartname değil.
-1e307...1e307ve tarih aralığı Swift
Charts'ın bugünkü davranışından çıkarıldı. Gelecekteki bir işletim sistemi bunu değiştirebilir;
o durumda aşırı girdi süitleri kırılıp bunu gösterir.
- Sınıra çekme görünür. ±1e307'nin ötesindeki değerler o sınırda çiziliyor; bu yüzden böyle iki
değer ekranda ayırt edilemiyor, ama readout ve VoiceOver gerçek değeri söylüyor.
- Benchmark yok. 1.000 noktalık varsayılanın ve kaydırma penceresi sınırının gerekçesi var,
ama paketin bir benchmark süiti yok; eski Benchmarks/ klasörü hiçbir benchmark'ın üretmediği
sayıları raporluyordu ve yeniden kurulumda kaldırıldı.
- Taşınmayan grafikler: SwiftUI-Charts-Pro'daki Sankey, chord, violin, parallel coordinates ve
word cloud taşınmadı. Her biri TreemapLayout ve Histogram düzeyinde testlerle gelmesi gereken
kendi yerleşim ya da kestirim algoritmasını istiyor. Gauge, bubble, funnel, Gantt ve sunburst ise
SwiftUI'ın Gauge view'ı ya da birkaç Swift Charts mark'ı zaten karşıladığı için dışarıda kaldı.
- Zaman ekseninde pinch ile yakınlaştırma henüz yok; paketin Swift Package Index'te
listelenmesiyle birlikte yol haritasında.
Paketin geçmişi repoda görünüyor: Ağustos 2025'te açıldı, Şubat 2026'da 1.0.0 yayınlandı, Haziran
2026'da yer tutucu koda indirildi ve Ekim 2026'da 2.0.0 olarak yeniden kuruldu. CHANGELOG, 1.0.0'ın
güncel Xcode sürümleriyle derlenmediğini açıkça yazıyor.
Kaynak kod: github.com/muhittincamdali/SwiftUI-Data-Visualization

