[iOS] Swift Chartsでグラフを表示してみる

2023.01.20

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは。きんくまです。

iOS 16からSwift Chartsが加わったので、グラフを表示してみたいです。

グラフの元データを気象庁から取得

サンプルデータとして、気象庁からデータをダウンロードします。

過去の気象データ・ダウンロード

ダウンロードしたCSVはmacのExcelで開くと文字化けしたので、テキストエディタでUTF8(BOMあり)に文字コードを変換しておきました。
見せたい項目に編集したCSVデータです。

weather_data.csv

年月日,曜日,平均気温(℃),降水量の合計(mm)
2022/11/08,火,15.9,0
2022/11/09,水,14.6,0
2022/11/10,木,15.2,0
2022/11/11,金,16.1,0
2022/11/12,土,16.6,0
2022/11/13,日,18,0.5
2022/11/14,月,16.1,0
2022/11/15,火,11.1,6
2022/11/16,水,12.4,0
2022/11/17,木,12.8,0
2022/11/18,金,13.2,0
2022/11/19,土,13.7,0
2022/11/20,日,11.6,6
2022/11/21,月,13.4,3
2022/11/22,火,15.4,0
2022/11/23,水,11.7,42
2022/11/24,木,15.3,0.5
2022/11/25,金,13.5,0.5
2022/11/26,土,12.7,1
2022/11/27,日,14.4,0
2022/11/28,月,11.7,0
2022/11/29,火,16.7,10.5
2022/11/30,水,16.6,32.5
2022/12/01,木,10.1,0.5
2022/12/02,金,9.4,0
2022/12/03,土,7.8,0
2022/12/04,日,10.2,0
2022/12/05,月,7.6,19.5
2022/12/06,火,6.1,9.5
2022/12/07,水,8.5,0
2022/12/08,木,9.3,0

読み込み用のModel作成

グラフのModel

struct DailyWeather: Identifiable {
    var id = UUID()
    /// 年月日 2022/11/09
    var yearMonthDay: String
    /// 曜日
    var weekday: String
    /// 日時
    var date: Date
    /// 平均気温
    var averageTemperature: Double
    /// 降水量の合計
    var precipitationTotal: Double
}

CSVを読み込むローダー

class DailyWeatherLoader {
    
    func load() async -> [DailyWeather]? {
        guard let url = Bundle.main.url(forResource: "weather_data", withExtension: "csv") else {
            return nil
        }
        var dailyWeathers: [DailyWeather]?
        do {
            let data = try Data(contentsOf: url)
            let csvStr = String(data: data, encoding: .utf8)
            // 行ごとに分解
            guard let lines = csvStr?.split(separator: "\r\n").map({ String($0) }) else {
                return nil
            }
            // 列ごとに分解
            dailyWeathers = lines.map { line in
                let columns = line.split(separator: ",").map({ String($0) })
                let yearMonthDay = columns[0]
                let weekday = columns[1]
                let formatter = DateFormatter()
                formatter.locale = Locale(identifier: "ja")
                formatter.timeZone = TimeZone(identifier: "Asia/Tokyo")
                formatter.dateFormat = "yyyy/MM/dd"
                guard let date = formatter.date(from: yearMonthDay ),
                      let averageTemperature = Double(columns[2]),
                      let precipitationTotal = Double(columns[3]) else {
                    return nil
                }
                return DailyWeather(yearMonthDay: yearMonthDay, weekday: weekday, date: date, averageTemperature: averageTemperature, precipitationTotal: precipitationTotal)
            }.compactMap({ $0 })
        } catch {
            return nil
        }
        return dailyWeathers
    }
}

アプリ用のViewModel

class WeatherModel: ObservableObject {
    
    @Published var dailyWeathers: [DailyWeather] = []
    
    func loadWeather() {
        let loader = DailyWeatherLoader()
        Task {
            let result = await loader.load()
            if let dailyWeathers = result {
                Task.detached { @MainActor [weak self] in
                    self?.dailyWeathers = dailyWeathers
                }
            }
        }
    }
}

降水量の合計の棒グラフ

降水量の合計の棒グラフを作ってみます

import SwiftUI
import Charts

struct ContentView: View {
    
    @StateObject var weatherModel: WeatherModel = WeatherModel()
    
    var body: some View {
        Chart {
            ForEach(weatherModel.dailyWeathers) { weather in
                BarMark(
                    x: .value("date", weather.date),
                    y: .value("precipitationTotal", weather.precipitationTotal)
                )
            }
        }
        .chartYScale(domain: 0 ... 50)
        .chartXAxis {
            AxisMarks(position: .bottom, values: .stride(by: .day, count: 2)) { value in
                AxisValueLabel() {
                    if let weather = weatherModel.dailyWeathers[value.index] {
                        Text(weather.yearMonthDay).font(.system(size: 8))
                            .frame(width: 50, height: 30, alignment: .bottom)
                            .offset(x: -10, y: -20)
                            .rotationEffect(.degrees(-90))
                    }
                }
            }
        }
        .chartYAxisLabel("降水量の合計(mm)", position: .trailing, alignment: .center, spacing: 0)
        .chartXAxisLabel("年月日", position: .bottom, alignment: .top, spacing: 30)
        .padding(EdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 20))
        .onAppear(perform: {
            weatherModel.loadWeather()
        })
    }
}

平均気温の折れ線グラフ

平均気温の折れ線グラフを作ってみます

import SwiftUI
import Charts

struct ContentView: View {
    
    @StateObject var weatherModel: WeatherModel = WeatherModel()
    
    var body: some View { 
        Chart {
            ForEach(weatherModel.dailyWeathers) { weather in
                LineMark(
                    x: .value("date", weather.date),
                    y: .value("averageTemperature", weather.averageTemperature)
                )
            }
        }
        .chartXAxis {
            AxisMarks(position: .bottom, values: .stride(by: .day, count: 2)) { value in
                AxisValueLabel() {
                    if let weather = weatherModel.dailyWeathers[value.index] {
                        Text(weather.yearMonthDay).font(.system(size: 8))
                            .frame(width: 50, height: 30, alignment: .leading)
                            .offset(x: -10, y: -20)
                            .rotationEffect(.degrees(-90))
                    }
                }
            }
        }
        .chartYAxisLabel("平均気温(℃)", position: .trailing, alignment: .center, spacing: 0)
        .chartXAxisLabel("年月日", position: .bottom, alignment: .top, spacing: 30)
        .padding(EdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 20))
        .onAppear(perform: {
            weatherModel.loadWeather()
        })
    }
}

X軸の刻みの表示がちょっとおかしいのですが、すみません!
参考になれば幸いです。