NRIネットコム Blog

NRIネットコム社員が様々な視点で、日々の気づきやナレッジを発信するメディアです

【SwiftUI】Chartsフレームワークを使用してグラフを作成する方法

概要

iOS16から使用できるようになったChartsフレームワークについて使用方法と大まかな概要を説明した記事になります。

https://developer.apple.com/documentation/charts/creating-a-chart-using-swift-charts

はじめに

Chartとグラフが混同して出てきますが、コードに関する部分ではChartと表現し、図として表現したい場合はグラフとしています。

Chart

まずはドキュメントを参考にした簡単な棒グラフのサンプルコードとプレビューです。

import SwiftUI
import Charts

struct ChartsSampleView: View {
    var data: [ToyShape] = [
        .init(type: "Cube", count: 5),
        .init(type: "Sphere", count: 4),
        .init(type: "Pyramid", count: 4)
    ]

    var body: some View {
        Chart {
            BarMark(
                x: .value("Shape Type", data[0].type),
                y: .value("Total Count", data[0].count)
            )
            BarMark(
                 x: .value("Shape Type", data[1].type),
                 y: .value("Total Count", data[1].count)
            )
            BarMark(
                 x: .value("Shape Type", data[2].type),
                 y: .value("Total Count", data[2].count)
            )
        }
    }
}

struct ChartsSampleView_Previews: PreviewProvider {
    static var previews: some View {
        ChartsSampleView()
    }
}

struct ToyShape: Identifiable {
    var type: String
    var count: Double
    var id = UUID()
}

Chartを利用するにはChartsライブラリのImportが必要です。
使用方法はChart内にMarkを使用することでChartを表示することができます。
上記のサンプルではBarMarkを使用して棒グラフを表示していますが、棒グラフ一つ毎にBarMarkの呼び出しが必要となります。
またx: .value()には横軸にStringを、y: .value( )には縦軸でDoubleの値を入れることで縦棒グラフができます。(横棒グラフに関してはBarMarkのセクションで説明します)
基本的なChartの使用に関してはこれだけで、簡単にグラフを作成することができます。

MarkとProperty

このBarMarkについては各項目の視覚的要素の一種であり、それらの視覚要素を総称してMarkと呼びます。
Markについては6種類ありますが、そちらに関しては後ほど説明します。
またそれぞれのMarkに対して使用できるPropertyがあり、それらを効果的に使用することでより様々なグラフやChartを作成することができます。

https://developer.apple.com/videos/play/wwdc2022/10136/

上記サンプルでxやyが出てきましたがこれらはX PositionとY Positionにあたります。

foregroundStyle

foregroundStyleを使用すると棒に色をつけることができます。
ただ色の指定はforegroundStyleではできないのでchartForegroundStyleScaleで定義する必要があります。
以下積み上げ棒グラフのサンプルコードを参考にforegroundStyleを使用したサンプルです。

import SwiftUI
import Charts

struct ChartsSampleView: View {
    var stackedBarData: [ToyShape] = [
        .init(color: "Green", type: "Cube", count: 2),
        .init(color: "Green", type: "Sphere", count: 0),
        .init(color: "Green", type: "Pyramid", count: 1),
        .init(color: "Purple", type: "Cube", count: 1),
        .init(color: "Purple", type: "Sphere", count: 1),
        .init(color: "Purple", type: "Pyramid", count: 1),
        .init(color: "Pink", type: "Cube", count: 1),
        .init(color: "Pink", type: "Sphere", count: 2),
        .init(color: "Pink", type: "Pyramid", count: 0),
        .init(color: "Yellow", type: "Cube", count: 1),
        .init(color: "Yellow", type: "Sphere", count: 1),
        .init(color: "Yellow", type: "Pyramid", count: 2)
    ]
    var body: some View {
        Chart {
            ForEach(stackedBarData) { shape in
                BarMark(
                    x: .value("Shape Type", shape.type),
                    y: .value("Total Count", shape.count)
                )
                .foregroundStyle(by: .value("Shape Color", shape.color))
            }
        }
        .chartForegroundStyleScale ([
            "Green": .green, "Purple": .purple, "Pink": .pink, "Yellow": .yellow
        ])
        .padding()
    }
}

struct ChartsSampleView_Previews: PreviewProvider {
    static var previews: some View {
        ChartsSampleView()
    }
}

struct ToyShape: Identifiable {
    var color: String
    var type: String
    var count: Double
    var id = UUID()
}

stackedBarDataのようにデータの数が増えてくるとChart内で定義するBarMarkの数も比例して多くなります。
そこでForEachを使用して繰り返し処理を行います。
余談ですがForEachでstructを使用するにはIdentifiableに準拠させる必要があります。
またcolorを追加することで凡例が追加されます。
ForEachを使わず、Chartに直接引数としてstackedBarDataを渡して、クロージャーの引数としてToyShape型を定義することもできます。

Chart(stackedBarData) { shape in
    BarMark(
        x: .value("Shape Type", shape.type),
        y: .value("Total Count", shape.count)
    )
    .foregroundStyle (by: .value("Shape Color", shape.color))
}

annotation

ChartにTextやImageを注釈としてつけることができます。

Chart(stackedBarData) { shape in
    BarMark(
        x: .value("Shape Type", shape.type),
        y: .value("Total Count", shape.count)
    )
    .foregroundStyle (by: .value("Shape Color", shape.color))
    .annotation(position: .top) {
        shape.type == "Cube" ? Text("★") : Text("☆")
    }
}

annotationの引数にpositionを指定することで配置を変えることもできます。

SymbolやLine Styleに関しては以下の「Markの種類」セクション内で説明します。

Markの種類

https://developer.apple.com/videos/play/wwdc2022/10137

Markの種類はBarを含めて以下6種類あります。

  • AreaMark
  • LineMark
  • PointMark
  • RectangleMark
  • RuleMark
  • BarMark

以下それぞれの使用方法と特徴です。
※BarMark以外の各Markにて共通して使用するサンプルデータは以下の通りです。

struct SampleData: Identifiable {
    var id: String { name }
    let name: String
    let amount: Double
    let from: String
}

let sampleData: [SampleData] = [
    .init(name: "NameA", amount: 2500, from: "PlaceA"),
    .init(name: "NameB", amount: 3500, from: "PlaceA"),
    .init(name: "NameC", amount: 2000, from: "PlaceA"),
    .init(name: "NameD", amount: 4500, from: "PlaceA"),
    .init(name: "NameE", amount: 5000,from: "PlaceA"),
    .init(name: "NameF", amount: 5500,from: "PlaceA"),
    .init(name: "NameA", amount: 360, from: "PlaceB"),
    .init(name: "NameB", amount: 640, from: "PlaceB"),
    .init(name: "NameC", amount: 680, from: "PlaceB"),
    .init(name: "NameD", amount: 760, from: "PlaceB"),
    .init(name: "NameE", amount: 780, from: "PlaceB"),
    .init(name: "NameF", amount: 800, from: "PlaceB")
]

AreaMark

面グラフを作成することができます。
以下サンプルコードとプレビューです。

struct AreaMarkView: View {
    var body: some View {
        Chart(sampleData) { data in
            AreaMark(
                x: .value("Name", data.name),
                y: .value("Amount", data.amount)
            )
            .foregroundStyle(by: .value("From", data.from))
            .position(by: .value("From", data.from))
        }
        .chartForegroundStyleScale([
            "PlaceA": .green.opacity(0.4), "PlaceB": .purple.opacity(0.4)
        ])
        .frame(height: 300)
        .padding()
    }
}

RuleMark

RuleMarkに関しては少し特殊で、単体で使用してグラフを表示するものではなく、単一の水平線または垂直線を表示します。
以下サンプルコードとプレビューです。

struct RuleMarkView: View {
    var body: some View {
        Chart(sampleData) { data in
            AreaMark(
                x: .value("Name", data.name),
                y: .value("Amount", data.amount)
            )
            .foregroundStyle(by: .value("From", data.from))
            .position(by: .value("From", data.from))
            
            RuleMark(y: .value("Base", 2500))
                .lineStyle(StrokeStyle(lineWidth: 2))
                .annotation(position: .top, alignment: .leading) {
                    Text("Base Line 2500")
                        .font(.subheadline)
                        .foregroundColor(.blue)
                }
        }
        .chartForegroundStyleScale([
            "PlaceA": .green.opacity(0.4), "PlaceB": .purple.opacity(0.4)
        ])
        .frame(height: 300)
        .padding()
    }
}

annotationを使用すると基準となるラインなどに使用できます。

LineMark

折れ線グラフを作成することができます。
以下サンプルコードとプレビューです。

struct LineMarkView: View {
    var body: some View {
        Chart(sampleData) { data in
            LineMark(
                x: .value("Name", data.name),
                y: .value("Amount", data.amount)
            )
            .foregroundStyle(by: .value("Form", data.from))
            .lineStyle(StrokeStyle(lineWidth: 1))
        }
        .frame(height: 300)
        .padding()
    }
}

lineStyle(StrokeStyle())で線の太さなどを変化させたり.interpolationMethod()で滑らかな線や直線的な線に変更することができます。

左から順にstepCenter / catmullRom / linear

symbol()を使用すると各値にマーカーのようなシンボルを表示することができます。

struct LineMarkView: View {
    var body: some View {
        Chart(sampleData) { data in
            LineMark(
                x: .value("Name", data.name),
                y: .value("Amount", data.amount)
            )
            .foregroundStyle(by: .value("Name", data.from))
            .lineStyle(StrokeStyle(lineWidth: 1))
            .interpolationMethod(.linear)
            .symbol(by: .value("Form", data.from))
        }
        .frame(height: 300)
        .padding()
    }
}

PointMark

点グラフを作成することができます。
以下サンプルコードとプレビューです。

struct RectangleMarkView: View {
    var body: some View {
        Chart(sampleData) { data in
            PointMark(
                x: .value("Name", data.name),
                y: .value("Amount", data.amount)
            )
            .foregroundStyle(by: .value("Form", data.from))
        }
        .frame(height: 300)
        .padding()
    }
}

LineMarkと併用して使用することでsymbolのように値にマーカーとして表示することも可能です。

struct PointMarkView: View {
    var body: some View {
        Chart(sampleData) { data in
            LineMark(
                x: .value("Name", data.name),
                y: .value("Amount", data.amount)
            )
            .foregroundStyle(by: .value("Form", data.from))
            .lineStyle(StrokeStyle(lineWidth: 3))
            .interpolationMethod(.linear)
            PointMark(
                x: .value("Name", data.name),
                y: .value("Amount", data.amount)
            )
            .foregroundStyle(by: .value("Form", data.from))
        }
        .chartForegroundStyleScale([
                    "PlaceA": .green.opacity(0.4), "PlaceB": .purple.opacity(0.4)
                ])
        .frame(height: 300)
        .padding()
    }
}

RectangleMark

線表を作成することができます。
以下サンプルコードとプレビューです。

struct RectangleMarkView: View {
    var body: some View {
        Chart(sampleData) { data in
            RectangleMark(
                x: .value("Name", data.name),
                y: .value("Amount", data.amount)
            )
            .foregroundStyle(by: .value("Form", data.from))
        }
        .frame(height: 300)
        .padding()
    }
}

BarMark

棒グラフを作成することができます。
以下サンプルコードとプレビューです。

struct BarMarkView: View {
    let barColor: [Color] = [.red, .purple, .blue, .green, .yellow]
    var body: some View {
        Chart(productData) { data in
            BarMark(
                x: .value("ProductName", data.name),
                y: .value("Count", data.count)
            )
            .foregroundStyle(by: .value("ProductName", data.name))
        }
        .chartForegroundStyleScale(
            domain: productData.compactMap({ product -> String? in
                product.name
            }),
            range: barColor
        )
        .frame(height: 300)
        .padding()
    }
}

struct BarMarkView_Previews: PreviewProvider {
    static var previews: some View {
        BarMarkView()
    }
}

struct Products: Identifiable {
    var id: String { name }
    let name: String
    let count: Double
}

let productData: [Products] = [
    .init(name: "製品A", count: 960),
    .init(name: "製品B", count: 420),
    .init(name: "製品C", count: 660),
    .init(name: "製品D", count: 540),
    .init(name: "製品E", count: 1120)
]

XとYの値を反転させると横棒グラフになります。

BarMark(
    x: .value("Count", data.count),
    y: .value("ProductName", data.name)
)

幅を指定することもできます。

BarMark(
    x: .value("Shape Type", data[0].type),
    y: .value("Total Count", data[0].count),
    width: 10
)

また積み上げ棒グラフではなく同じ項目内で比較したいグラフを表示したい時は.positionを使用します。

struct BarMarkView: View {
    var body: some View {
        Chart(productData) { data in
            BarMark(
                x: .value("Count", data.count),
                y: .value("ProductName", data.name),
            )
            .foregroundStyle(by: .value("ProductName", data.factory))
            .position(by: .value("Factory", data.factory))
        }
        .chartForegroundStyleScale([
            "工場A": .green, "工場B": .purple
        ])
        .frame(height: 300)
        .padding()
    }
}

struct BarMarkView_Previews: PreviewProvider {
    static var previews: some View {
        BarMarkView()
    }
}

struct Products: Identifiable {
    var id: String { name }
    let name: String
    let count: Double
    let factory: String
}

let productData: [Products] = [
    .init(name: "製品A", count: 960, factory: "工場A"),
    .init(name: "製品B", count: 420, factory: "工場A"),
    .init(name: "製品C", count: 660, factory: "工場A"),
    .init(name: "製品D", count: 540, factory: "工場A"),
    .init(name: "製品E", count: 1120, factory: "工場A"),
    .init(name: "製品A", count: 360, factory: "工場B"),
    .init(name: "製品B", count: 640, factory: "工場B"),
    .init(name: "製品C", count: 780, factory: "工場B"),
    .init(name: "製品D", count: 960, factory: "工場B"),
    .init(name: "製品E", count: 880, factory: "工場B")
]

最後に

ざっとChartについての説明からかくMarkについてサンプルコードとプレビューで紹介させていただきました。
次回はもっと複雑な使用方法について掘り下げたいと思います。

参考記事

https://developer.apple.com/documentation/charts/creating-a-chart-using-swift-charts

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

https://developer.apple.com/documentation/charts/barmark/init(x:y:width:height:stacking:)

執筆者岡優志

iOSエンジニア
iOSを専門とし、モバイルアプリの開発を行なっています。

Twitter