JetpackCompose Vico(v2.0.0)を使ったグラフ描画の実装サンプル

## はじめに

業務でvicoというグラフ描画ライブラリを使ったので、簡単な実装サンプルを共有します。

## Vicoとは

vicoとはグラフを描画するために使用するライブラリです。

Androidでグラフを実装する場合、公式には提供されていないため、ライブラリを頼る必要が、いくつか調べた中で
vicoは複雑なグラフも描画できる、良さそうってことで選択しました。

以下のVico を選択した理由です。

- 	AndroidView と Jetpack Compose の両方に対応
- 	メンテナンスが頻繁に行われている
- 	公式ドキュメントやサンプルアプリが充実

ちなみにiOSでは、[Swift Charts](https://developer.apple.com/documentation/charts)というApple純正のものが提供されています。

公式ドキュメント:
https://www.patrykandpatrick.com/vico/guide/stable

サンプルアプリもあるので、どんなグラフが描画できるのか簡単にイメージできます。
サンプルアプリ↓

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520693/fa4d2a74-91ae-deea-91b6-730a7f3ba2a2.gif" width="400">

ほぼメンテナンスが止まってしまっているのが 残念で、JetpackCompose に対応していないものもありますが、vico 意外にもいくつかライブラリがあったので載せておきます。

- [MPAndroidChart](https://github.com/PhilJay/MPAndroidChart)
   - 老舗。今はメンテ止まってしまっている

- [charts](https://github.com/tehras/charts)
   - composeにも対応しているが、これもメンテが止まっている
   
- [YCharts](https://github.com/codeandtheory/YCharts)
   - こちらもcomposeに対応しているが、2年前にメンテが止まっている



# Vicoの実装

## 1. 準備
公式ドキュメントの[getting started](https://www.patrykandpatrick.com/vico/guide/stable/getting-started)に従い
version catalogに依存関係を定義します。
(version catalogを使わない方法も記載があります。)

注意:
Vico 2系のStable版が2025年1月20日にリリース されました。1系とはクラス構造が大きく異なり、 Breaking Change が発生しているため、移行には注意が必要そうです。今回は２系で動く実装サンプルです。


```gradle
[versions]
vico = "2.0.1"
 
[libraries]
vico-compose = { group = "com.patrykandpatrick.vico", name = "compose", version.ref = "vico" }
 
vico-compose-m2 = { group = "com.patrykandpatrick.vico", name = "compose-m2", version.ref = "vico" }
 
vico-compose-m3 = { group = "com.patrykandpatrick.vico", name = "compose-m3", version.ref = "vico" }
 
vico-views = { group = "com.patrykandpatrick.vico", name = "views", version.ref = "vico" }
```

## 2. グラフの作成（棒グラフ）
サンプルを参考に、シンプルな 棒グラフ を作成します。

``` kotlin
@Composable
fun VicoBarGraph(
    modifier: Modifier = Modifier
) {
    CartesianChartHost(
        chart =
        rememberCartesianChart(
            rememberColumnCartesianLayer(),
            startAxis = VerticalAxis.rememberStart(),
            bottomAxis = HorizontalAxis.rememberBottom(),
        ),
        model = CartesianChartModel(
            ColumnCartesianLayerModel.build { series(1, 2, 4, 8, 3, 10, 4, 7, 2, 6, 4, 8) }
        ),
    )
}

@Preview
@Composable
fun PreVicoBarGraph() {
    VicoBarGraph()
}
```

出力されるグラフ

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520693/c2279213-782b-58e3-9a18-0cc01ab692a5.png" width="400">


## 3. グラフのカスタマイズ

### (1) Y軸のカスタマイズ

y軸のカスタマイズはstartAxisにVerticalAxis渡すことでさまざまなカスタマイズができるようです。

```
startAxis = VerticalAxis.rememberStart() // rememberStartにさまざまな設定値を渡せる
```

**①y軸のラベルのスタイルを調整**
labelにTextComponentを渡す

```kotlin
startAxis = VerticalAxis.rememberStart(
    // y軸の目盛ラベルのスタイルを調整
    label = rememberTextComponent(
        color = Color.Red, // 目盛ラベルの色
        textSize = 12.sp, // フォントサイズ
    ),
)
```


出力結果:
他にも通常のTextコンポーネントの同じような設定(lineHeight、textAlignment)も調整が可能。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520693/267da879-efd1-b71f-7b98-acddfec20fa7.png" width="500">

**②y軸のスタイルを調整**

lineにLineComponentを渡す

```kotlin
startAxis = VerticalAxis.rememberStart(
    // y軸のスタイルを調整
    line = rememberLineComponent(
        fill = fill(Color.Blue), // y軸の色
    ),
)
```

出力結果:
他にもy軸の太さなども変更可能。

![スクリーンショット 2025-02-02 14.00.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520693/83aaf407-317e-44bc-1965-c9f8759c6e36.png)

**③y軸の補助線のスタイルを調整**

guidelineにLineComponentを渡す

```kotlin
startAxis = VerticalAxis.rememberStart(
    // 補助線のスタイルを調整
    guideline = rememberLineComponent(
        fill = fill(Color.Blue)
    ),
)
```
出力結果:
他にも、補助線を実線や点線にすることなどもできます。

![スクリーンショット 2025-02-02 14.04.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520693/a323bb92-e5fb-b95c-8906-3f7b747c0137.png)

**④y軸の目盛線のスタイルを調整**

tickにLineComponentを渡す

```kotlin
startAxis = VerticalAxis.rememberStart(
    // y軸の目盛のスタイルを調整、数字の横に出る線
    tick = rememberLineComponent(
        fill = fill(Color.Green),                
    ),                            
)
```
出力結果:
少しわかりづらいですが、y軸の目盛の線が変わっています。

![スクリーンショット 2025-02-02 14.16.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520693/c226f42b-5e76-4948-9d4d-583b2d5ee8e5.png)

### (2) x軸をカスタマイズ

x軸も同じようにy軸とほぼ同じでbottomAxisにHorizontalAxisを渡すことでさまざまなカスタマイズができるようです。

```
bottomAxis = HorizontalAxis.rememberBottom(),　// rememberBottomにさまざまな設定値を渡せる
```
**①x軸の項目ラベルのスタイルを調整**

labelにTextComponentを渡す

```kotlin
bottomAxis = HorizontalAxis.rememberBottom(
    // x軸の項目ラベルのスタイルを調整
    label = rememberTextComponent(
        color = Color.Red, // 項目ラベルの色
        textSize = 12.sp, // フォントサイズ
    ),
)
```
出力結果:
![スクリーンショット 2025-02-02 14.25.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520693/6c861c04-fd48-11ee-67f4-22f46b0bd7eb.png)

**②x軸のスタイルを調整**

lineにLineComponentを渡す

```kotlin
bottomAxis = HorizontalAxis.rememberBottom(
    // x軸のスタイルを調整
    line = rememberLineComponent(
        fill = fill(Color.Red)
    ),
)
```
出力結果:
![スクリーンショット 2025-02-02 14.30.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520693/ce47f871-1a4c-1a83-b7b9-00650b48e3c8.png)



**③補助線のスタイルを調整**

guidelineにLineComponentを渡す。

```kotlin
bottomAxis = HorizontalAxis.rememberBottom(
    // 補助線のスタイルを調整
    guideline = rememberLineComponent(
        fill = fill(Color.Red),
        shape = dashedShape(), // 点線にする
    ),
)
```
出力結果:
![スクリーンショット 2025-02-02 14.32.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520693/e0fb4477-d34d-4e85-d662-bde0501ef009.png)

**④x軸のラベル名を任意の文字列に変更**

valueFormatterに任意の文字列を渡す。

```kotlin
val xAxisLabels = listOf("A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L")

bottomAxis = HorizontalAxis.rememberBottom(
    // x軸のラベル名の文字列を調整
    valueFormatter = { _, x, _ ->
        xAxisLabels[x.toInt() % xAxisLabels.size]
    }
)

```
出力結果:

![スクリーンショット 2025-02-02 14.38.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520693/455b76a1-b81b-6235-9a3b-1793a6778bc9.png)

**⑤x軸のラベル名の表示パターン調整**

itemPlacerにHorizontalAxis.ItemPlacerを渡す

```kotlin
bottomAxis = HorizontalAxis.rememberBottom(
    itemPlacer = HorizontalAxis.ItemPlacer.aligned(
        spacing = { 5 }, // x軸のラベルを何個ごとに表示するか
        offset = { 1 }, // x軸のラベルのどこから表示を始めるか
    ),
)
```
出力結果:

![スクリーンショット 2025-02-02 14.43.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520693/4c677d63-9a02-6ed0-83d7-8ea088e140ae.png)

## 4. 棒線をカスタマイズ
 
**①棒線のスタイルを調整**

columnProviderにColumnCartesianLayerを渡すことで調整が可能

```kotlin
rememberColumnCartesianLayer(
    columnProvider = ColumnCartesianLayer.ColumnProvider.series(
        rememberLineComponent(
            fill = fill(Color.Red), // 棒線の色
            thickness = 8.dp, // 棒線の太さ
            shape = CorneredShape.rounded(
                topLeftPercent = 40, // 棒線の角丸
                topRightPercent = 40 // 棒線の角丸
            ),
        )
    ),
),
```

![スクリーンショット 2025-02-02 14.52.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520693/ea1e8871-710a-dadd-ddee-1b2ead590ece.png)

**②棒線の表示間隔を調整**

columnCollectionSpacingに値をセット

```kotlin
rememberColumnCartesianLayer(
    columnCollectionSpacing = 5.dp, // 各棒線の間のスペース
),
```

![スクリーンショット 2025-02-02 14.56.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520693/70b16ade-460a-476e-0b13-a29eab11b243.png)

## 5. グラフのスクロール

グラフをスクロール可能にするには VicoScrollState を設定します。

```kotlin
CartesianChartHost(
    model = cartesianChartModel,
    scrollState = rememberVicoScrollState(
        scrollEnabled = true , // スクロール可能か
        initialScroll = Scroll.Absolute.Start, // スクロール可能な場合に初期表示をどこから開始するか
    ),
)
```

スクロール動作:

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520693/92db1abe-af94-e9cb-e396-57eb4a950078.gif" width="400">


### 注意点

**アニメーション**

グラフをアニメーションさせるにはCartesianChartModelProducerから、LaunchedEffectを使ってデータを渡すような実装にする必要があります。しかし、そうすると、プレビューでグラフが確認できなくなるので、以下のようにプレビューとビルド時の処理を分けました。

```kotlin
@Composable
fun VicoBarGraph(
    modifier: Modifier = Modifier
) {
    val isPreview = LocalInspectionMode.current

    val data = listOf(1, 2, 4, 8, 3, 10, 4, 7, 2, 6, 4, 8, 9, 10, 11, 12)

    val chart = rememberCartesianChart(
        rememberColumnCartesianLayer(),
        startAxis = VerticalAxis.rememberStart(),
        bottomAxis = HorizontalAxis.rememberBottom()
    )

    if (isPreview) {
        // プレビューでグラフを確認するためにはCartesianChartModelを使う必要がある
        val cartesianChartModel = remember {
            CartesianChartModel(
                ColumnCartesianLayerModel.build { series(*data.toTypedArray()) }
            )
        }
        CartesianChartHost(
            chart = chart,
            model = cartesianChartModel
        )
    } else {
        // modelProducerを使うと、プレビューではグラフが確認できなくなるが、
        // グラフのアニメーションをさせるために必要なので、分ける
        val modelProducer = remember { CartesianChartModelProducer() }
        LaunchedEffect(Unit) {
            modelProducer.runTransaction {
                columnSeries { series(*data.toTypedArray()) }
            }
        }
        CartesianChartHost(
            chart = chart,
            modelProducer = modelProducer,
        )
    }
}
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520693/8c15a8c0-4502-5ae1-b786-828b55cf2b1b.gif" width="300">


以上です。

## 所感

Vicoはグラフの描画自体は比較的簡単に実装可能 です。
しかし、カスタマイズに関しては 公式ドキュメントだけでは分かりにくい部分もあり、サンプルコードを直接読んで試行錯誤する必要がありました。ｘｙ軸の細かい設定や、複数のグラフを組みわ合わせることもできるような部分は単純にすごいと思いました。