Chart がきた
iOS には今まで標準ライブラリでチャートを表示させる方法がなく、外部ライブラリに依存しっぱなしだったのですがとうとう Swift にもチャートライブラリが実装されました。
正直、機能としてはまだまだ足りないところがあるのですが標準機能として実装されるのはありがたいですね。
今回はCreating a chart using Swift Chartsを読みながら、SwiftUI でチャートを実装させるための方法を学んでいこうと思います。
チャートの種類
チャートにはMark
と呼ばれるコンポーネントがあり、これでチャートを表現します。
- AreaMark
- LineMark
- PointMark
- RectangleMark
- RuleMark
- BarMark
実装されているのは上の六種類で、PieMark や DoughnutMark や RadarMark のようなものは存在しません。あればよかったんですけどね、残念です。
で、それぞれどんな実装が可能なのかを見ていきます。サンプルの画像は Apple のドキュメントから引っ張ってきました。
AreaMark

init<X, Y>(
x: PlottableValue<X>,
y: PlottableValue<Y>,
stacking: MarkStackingMethod = .standard
) where X : Plottable, Y : Plottable
株価とかに使えそうなやつですね。

これも積み重ねることができます。

積み重ね方を変えるとこういう表現も可能という例。
LineMark

init<X, Y>(
x: PlottableValue<X>,
y: PlottableValue<Y>
) where X : Plottable, Y : Plottable
標準的なラインチャートって感じがします。

線を増やすこともできます。
PointMark

init<X, Y>(
x: PlottableValue<X>,
y: PlottableValue<Y>
) where X : Plottable, Y : Plottable
散布図とかに使えるやつです。
RectangleMark

init<X, Y>(
x: PlottableValue<X>,
yStart: PlottableValue<Y>,
yEnd: PlottableValue<Y>,
width: MarkDimension = .automatic
) where X : Plottable, Y : Plottable
使い所がよくわからないグラフの一つ。
RuleMark

init<X, Y>(
x: PlottableValue<X>,
yStart: PlottableValue<Y>,
yEnd: PlottableValue<Y>
) where X : Plottable, Y : Plottable
これもいまいち使い所がわかっていないです。幅をもたせたいときに使う感じでしょうか。
BarMark

init<X, Y>(
x: PlottableValue<X>,
yStart: PlottableValue<Y>,
yEnd: PlottableValue<Y>,
width: MarkDimension = .automatic
) where X : Plottable, Y : Plottable
標準的なバーチャートです。使い所は多いはず。サンプルはこの向きですが、X 軸と Y 軸の反転や積み重ねなどもできます。

PlottableValue
で、チャートを表現するために必要なのがこのPlottableValue
という謎の構造体。
struct PlottableValue<Value> where Value: Plottable {
}
定義を見てみるとこんな感じで、要はPlottable
に適合すれば良いらしいので、Plottable
を調べると、
You can plot Plottable data values with marks with
.value(label, keyPath)
:
とでてきます。なんでこんなややこしいことになっているのかとも思ったのですが、よくよく考えれば理にかなっている気もします。
例えばラインチャートを使って以下の画像のようなものを表示させることを考えます。

このときに必要なのはx
の値をy
の値を持つ構造体の配列です。なので以下のようなコードを使ってEntry
を定義して、その配列を渡せばチャートの表示に必要なデータとしては十分なわけです。
struct Entry<T: BinaryFloatingPoint> {
let x: T
let y: T
}
let entries: [Entry] = []
var body: some View {
Chart(entries, content: {
LineMark(
x: .value(x)),
y: .value(y))
)
})
}
あれ、じゃあデータが一つ増えて二本の線を引くことになったらどうすればいいんだとなりますね。

バカ正直に考えるとentries
を二次元配列にして、二回描画するとかなるわけですがそれだと無駄にコードが複雑化してしまいます。
じゃあどうするのかというとentries
自体は弄らずに、線を区別するための情報をEntry
にもたせます。例として、以下のコードではtype
で区別するようにしました。
struct Entry<T: BinaryFloatingPoint> {
let x: T
let y: T
let type: Int
}
let entries: [Entry] = []
var body: some View {
Chart(entries, content: { entry in
LineMark(
x: .value(entry.x)),
y: .value(entry.y))
)
.foregroundStyle(by: .value(entry.type))
})
}
foregroundStyle
というのはまあ簡単に言えばカテゴリ分類みたいな感じです。上のコードでは簡略化のためにラベルを省略していますが、本来のPlottable
はラベルと値を組み合わせたプロトコルなので、ラベルのデータも必要になります。
とはいえ、使う場面があまりないのでなくてもいいのかもしれません。いや、多分要るけど理解ができてなさすぎて使う場面がわからないだけなんですけれど。