📈

【Swift】SwiftUIでグラフを実装する

2024/04/02に公開

初めに

今回はSwiftUIでグラフを実装していきたいと思います。

記事の対象者

  • Swift, SwiftUI 学習者
  • SwiftUIでグラフを実装したい方

目的

今回は上記の通りSwiftUIでグラフを実装することを目的とします。
最終的には以下の画像のようなグラフを実装します。

実装

実装は以下の手順で進めていきます。

  1. グラフに使用するデータ構造とサンプルデータの定義
  2. グラフのUIの実装
  3. ContentViewに反映

1. グラフに使用するデータ構造とサンプルデータの定義

まずはグラフに使用するデータ構造と表示させるサンプルデータを定義していきます。
今回は東京都の数年間の人口の推移を折れ線グラフで表示させたいと思います。
なお、人口データは e-Statの統計ダッシュボード から取得しました。

コードは以下の通りです。

import Foundation

struct PopulationTrendsLineData: Identifiable, Equatable, Hashable {
    var id = UUID()
    var year: Int
    var population: Int
}

let tokyoLineData: [PopulationTrendsLineData] = [
    .init(year: 1975, population: 11673554),
    .init(year: 1976, population: 11674000),
    .init(year: 1977, population: 11669000),
    .init(year: 1978, population: 11659000),
    .init(year: 1979, population: 11637000),
    .init(year: 1980, population: 11618281),
    // ~ 2022年までのデータ 長いので省略 ~
]

PopulationTrendsLineData では id として UUID を指定しており、自動的にIDが生成されるようになっています。また、year, population として特定の年の人口を格納しています。
そして、tokyoLineData にリストの PopulationTrendsLineData をサンプルデータとして格納しています。

全データを含むコード
import Foundation

struct PopulationTrendsLineData: Identifiable, Equatable, Hashable {
    var id = UUID()
    var year: Int
    var population: Int
}

let tokyoLineData: [PopulationTrendsLineData] = [
    .init(year: 1975, population: 11673554),
    .init(year: 1976, population: 11674000),
    .init(year: 1977, population: 11669000),
    .init(year: 1978, population: 11659000),
    .init(year: 1979, population: 11637000),
    .init(year: 1980, population: 11618281),
    .init(year: 1981, population: 11626000),
    .init(year: 1982, population: 11650000),
    .init(year: 1983, population: 11700000),
    .init(year: 1984, population: 11759000),
    .init(year: 1985, population: 11829363),
    .init(year: 1986, population: 11888000),
    .init(year: 1987, population: 11887000),
    .init(year: 1988, population: 11873000),
    .init(year: 1989, population: 11863000),
    .init(year: 1990, population: 11855563),
    .init(year: 1991, population: 11894000),
    .init(year: 1992, population: 11887000),
    .init(year: 1993, population: 11849000),
    .init(year: 1994, population: 11796000),
    .init(year: 1995, population: 11773605),
    .init(year: 1996, population: 11808000),
    .init(year: 1997, population: 11881000),
    .init(year: 1998, population: 11939000),
    .init(year: 1999, population: 11983000),
    .init(year: 2000, population: 12064101),
    .init(year: 2001, population: 12165000),
    .init(year: 2002, population: 12271000),
    .init(year: 2003, population: 12388000),
    .init(year: 2004, population: 12482000),
    .init(year: 2005, population: 12576601),
    .init(year: 2006, population: 12704000),
    .init(year: 2007, population: 12848000),
    .init(year: 2008, population: 12973000),
    .init(year: 2009, population: 13048000),
    .init(year: 2010, population: 13159388),
    .init(year: 2011, population: 13198000),
    .init(year: 2012, population: 13234000),
    .init(year: 2013, population: 13307000),
    .init(year: 2014, population: 13399000),
    .init(year: 2015, population: 13515271),
    .init(year: 2016, population: 13646000),
    .init(year: 2017, population: 13768000),
    .init(year: 2018, population: 13887000),
    .init(year: 2019, population: 14007000),
    .init(year: 2020, population: 14047594),
    .init(year: 2021, population: 14010000),
    .init(year: 2022, population: 14038000),
]

2. グラフのUIの実装

次にグラフのUIを実装していきます。
コードは以下の通りです。

import SwiftUI
import Charts

struct PopulationTrendView: View {
    let populationTrendData: [PopulationTrendsLineData]
    @State private var minYearXValue: Int = 0
    @State private var maxYearXValue: Int = 0
    @State private var minPopulationYValue: Int = 0
    @State private var maxPopulationYValue: Int = 0
    
    var body: some View {
        VStack {
            Text("\(minYearXValue + 1) ~ \(maxYearXValue - 1)年の人口推移")
                .font(.title)
            Chart(populationTrendData) { dataRow in
                LineMark(x: .value("Year", dataRow.year), y: .value("Population", dataRow.population))
            }
            .chartXScale(domain: minYearXValue...maxYearXValue)
            .chartYScale(domain: minPopulationYValue...maxPopulationYValue)
            .onAppear {
                minYearXValue = (populationTrendData.min { $0.year < $1.year }?.year ?? 0) - 1
                maxYearXValue = (populationTrendData.max { $0.year < $1.year }?.year ?? 0) + 1
                minPopulationYValue = (populationTrendData.min { $0.population < $1.population }?.population ?? 0) - 100000
                maxPopulationYValue = (populationTrendData.max {$0.population < $1.population }?.population ?? 0) + 100000
            }
        }
        .padding(50)
    }
}

それぞれ詳しくみていきます。

以下では populationTrendData として PopulationTrendsLineData のリストを受け取るようにしています。
また、グラフを表示させる際のグラフの縦軸、横軸の最小値、最大値の数値を計算するための値を State として保持しています。これに関しては後述します。

let populationTrendData: [PopulationTrendsLineData]
@State private var minYearXValue: Int = 0
@State private var maxYearXValue: Int = 0
@State private var minPopulationYValue: Int = 0
@State private var maxPopulationYValue: Int = 0

以下ではデータの year の最小値、最大値をもとにタイトルを表示させています。

Text("\(minYearXValue + 1) ~ \(maxYearXValue - 1)年の人口推移")
    .font(.title)

以下ではデータをもとに折れ線グラフの実装を行なっています。
引数として受け取った populationTrendDatadataRow として扱い、x軸のラベルを Year, y軸のラベルを Population としてそれぞれのデータを表示しています。

Chart(populationTrendData) { dataRow in
    LineMark(
        x: .value("Year", dataRow.year),
        y: .value("Population", dataRow.population)
    )
}

以下ではグラフの設定を行なっています。
具体的にはx軸、y軸のデータの範囲を絞っています。
コメントにもある通り、Viewが構築される段階でデータに含まれる year, population の最小値、最大値を求め、それを chartXScale, chartYScaledomain にそれぞれ代入することで、グラフの範囲を限定しています。
なお、表示領域に余裕を持たせるため、year は1年、population は10万ずつ足し引きをしています。

.chartXScale(domain: minYearXValue...maxYearXValue)
.chartYScale(domain: minPopulationYValue...maxPopulationYValue)
.onAppear {
    // データに含まれる year の最小値を計算
    minYearXValue = (populationTrendData.min { $0.year < $1.year }?.year ?? 0) - 1
    // データに含まれる year の最大値を計算
    maxYearXValue = (populationTrendData.max { $0.year < $1.year }?.year ?? 0) + 1
    // データに含まれる population の最小値を計算
    minPopulationYValue = (populationTrendData.min { $0.population < $1.population }?.population ?? 0) - 100000
    // データに含まれる population の最大値を計算
    maxPopulationYValue = (populationTrendData.max {$0.population < $1.population }?.population ?? 0) + 100000
}

これでグラフのUIの作成は完了です。

ちなみに... 仮にグラフのx軸、y軸のデータの範囲を絞らなかった場合は、以下の画像のように表示範囲が限定されず、見づらいグラフになります。(本来であればよしなに表示してくれるようなのですが、手元では以下のようになりました。)

3. ContentViewに反映

最後に ContentView に反映させます。
コードは以下の通りです。

import SwiftUI

struct ContentView: View {
    var body: some View {
        PopulationTrendView(populationTrendData: tokyoLineData)
            .glassBackgroundEffect()
    }
}

これで実行すると以下の画像のように折れ線グラフが表示されるかと思います。

今回のコード全文
import Foundation
import SwiftUI
import Charts

// content view で表示
struct ContentView: View {
    var body: some View {
        PopulationTrendView(populationTrendData: tokyoLineData)
            .glassBackgroundEffect()
    }
}

// グラフのUI
struct PopulationTrendView: View {
    let populationTrendData: [PopulationTrendsLineData]
    @State private var minYearXValue: Int = 0
    @State private var maxYearXValue: Int = 0
    @State private var minPopulationYValue: Int = 0
    @State private var maxPopulationYValue: Int = 0
    
    var body: some View {
        VStack {
            Text("\(minYearXValue + 1) ~ \(maxYearXValue - 1)年の人口推移")
                .font(.title)
            Chart(populationTrendData) { dataRow in
                LineMark(x: .value("Year", dataRow.year), y: .value("Population", dataRow.population))
            }
            .chartXScale(domain: minYearXValue...maxYearXValue)
            .chartYScale(domain: minPopulationYValue...maxPopulationYValue)
            .onAppear {
                minYearXValue = (populationTrendData.min { $0.year < $1.year }?.year ?? 0) - 1
                maxYearXValue = (populationTrendData.max { $0.year < $1.year }?.year ?? 0) + 1
                minPopulationYValue = (populationTrendData.min { $0.population < $1.population }?.population ?? 0) - 100000
                maxPopulationYValue = (populationTrendData.max {$0.population < $1.population }?.population ?? 0) + 100000
            }
        }
        .padding(50)
    }
}

// グラフに使用するデータのデータ構造
struct PopulationTrendsLineData: Identifiable, Equatable, Hashable {
    var id = UUID()
    var year: Int
    var population: Int
}

// サンプルデータ
let tokyoLineData: [PopulationTrendsLineData] = [
    .init(year: 1975, population: 11673554),
    .init(year: 1976, population: 11674000),
    .init(year: 1977, population: 11669000),
    .init(year: 1978, population: 11659000),
    .init(year: 1979, population: 11637000),
    .init(year: 1980, population: 11618281),
    .init(year: 1981, population: 11626000),
    .init(year: 1982, population: 11650000),
    .init(year: 1983, population: 11700000),
    .init(year: 1984, population: 11759000),
    .init(year: 1985, population: 11829363),
    .init(year: 1986, population: 11888000),
    .init(year: 1987, population: 11887000),
    .init(year: 1988, population: 11873000),
    .init(year: 1989, population: 11863000),
    .init(year: 1990, population: 11855563),
    .init(year: 1991, population: 11894000),
    .init(year: 1992, population: 11887000),
    .init(year: 1993, population: 11849000),
    .init(year: 1994, population: 11796000),
    .init(year: 1995, population: 11773605),
    .init(year: 1996, population: 11808000),
    .init(year: 1997, population: 11881000),
    .init(year: 1998, population: 11939000),
    .init(year: 1999, population: 11983000),
    .init(year: 2000, population: 12064101),
    .init(year: 2001, population: 12165000),
    .init(year: 2002, population: 12271000),
    .init(year: 2003, population: 12388000),
    .init(year: 2004, population: 12482000),
    .init(year: 2005, population: 12576601),
    .init(year: 2006, population: 12704000),
    .init(year: 2007, population: 12848000),
    .init(year: 2008, population: 12973000),
    .init(year: 2009, population: 13048000),
    .init(year: 2010, population: 13159388),
    .init(year: 2011, population: 13198000),
    .init(year: 2012, population: 13234000),
    .init(year: 2013, population: 13307000),
    .init(year: 2014, population: 13399000),
    .init(year: 2015, population: 13515271),
    .init(year: 2016, population: 13646000),
    .init(year: 2017, population: 13768000),
    .init(year: 2018, population: 13887000),
    .init(year: 2019, population: 14007000),
    .init(year: 2020, population: 14047594),
    .init(year: 2021, population: 14010000),
    .init(year: 2022, population: 14038000),
]

まとめ

最後まで読んでいただいてありがとうございました。

今回は SwiftUI でグラフを実装する方法を簡単に共有しました。
他にも棒グラフや円グラフも実装できるようなので、機会があれば追加していきます。

誤っている点等あればご指摘いただければ幸いです。

参考

https://developer.apple.com/documentation/charts/linemark

Discussion