こんにちは。きんくまです。
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軸の刻みの表示がちょっとおかしいのですが、すみません!
参考になれば幸いです。