Vega解説 〜Reactアプリ上でVegaグラフを表示し、グラフのイベントを捕まえる〜

Vega 解説記事です。[今まで](#参考)はVegaでのグラフの書き方を説明していましたが、今回はWebアプリに組み込んでみます。

完成イメージは以下です。

1. データを渡してバーチャートを描画
2. バーチャートのバーをクリックすると、設定されている値を取得しテキストボックスに設定
3. テキストボックスに入った値を更新し、Updateボタンをクリックするとバーチャートのグラフを更新

Vega グラフとHTMLフォームのイベントを捕捉し、相互に更新します。

react-vega というライブラリもありますが、今回は素の[Vega View API](https://vega.github.io/vega/docs/api/view/) を利用して実現します。


### barchartSpec.ts 

Vega Specを定義し返却します。Specの内容は[Vega解説〜離散データ〜](https://qiita.com/ihgs/items/87fda0cd39693bae44f3)を流用したものです。

```typescript
import * as vega from "vega";

export interface BarChartValue {
    category: string
    amount: number
}

export const barchartSpec = (values: BarChartValue[]): vega.Spec => {
    return {
        "$schema": "https://vega.github.io/schema/vega/v5.json",
        "width": 400,
        "height": 200,
        "padding": 5,
        "data": [
            {
                "name": "table",
                "values": values
            }
        ],
        "scales": [
            {
                "name": "xscale",
                "type": "band",
                "domain": { "data": "table", "field": "category" },
                "range": "width",
                "padding": 0.2,
                "round": true
            },
            {
                "name": "yscale",
                "domain": { "data": "table", "field": "amount" },
                "range": "height"
            }
        ],
        "axes": [
            { "orient": "bottom", "scale": "xscale" },
            { "orient": "left", "scale": "yscale" }
        ],
        "marks": [
            {
                "name": "layer_0",
                "type": "rect",
                "from": { "data": "table" },
                "encode": {
                    "update": {
                        "x": { "scale": "xscale", "field": "category" },
                        "width": { "scale": "xscale", "band": 1 },
                        "y": { "scale": "yscale", "field": "amount" },
                        "y2": { "scale": "yscale", "value": 0 },
                        "fill": { "value": "steelblue" }
                    }
                }
            }
        ]
    }
} 
```


### BarChart.tsx

今回はこのファイルがメインの処理になります。

* specを取得し、View APIを利用して描画します
   ```typescript
   const view = new vega.View(vega.parse(spec), {
       renderer: 'canvas',
       container: '#view',
       hover: true
   })
   view.runAsync()
   ```
* `view` に clickイベントを登録します
    ```typescript
    view.addEventListener('click', handleVegaClickEvent)
    ```
* クリックされたItemのデータを取得し、`category` と `amount` の状態を更新します
    ```typescript
    const handleVegaClickEvent = (_: vega.ScenegraphEvent, item?: vega.Item | null) => {
        if (item) {
            setCategory(item.datum.category)
            setAmount(item.datum.amount)
        }
    }
    ```
* ボタンを押した時に、categoryが一致するデータを探し入力された情報（amount）に変更します
    ```typescript
    const handleClick = () => {
        if (view) {
            view.change('table', vega.changeset().modify((datum: any) => {
                return datum.category === category
            }, 'amount', amount)).run()
        }
    }
    ```

全体は以下

```typescript
import { ChangeEvent, useEffect, useState } from "react"
import * as vega from "vega"
import { BarChartValue, barchartSpec } from "./barchartSpec"

export function BarChart({ values }: { values: BarChartValue[] }) {
    const [category, setCategory] = useState<string>()
    const [amount, setAmount] = useState<number>(0)
    const [view, setView] = useState<vega.View>()

    const spec = barchartSpec(values)

    useEffect(() => {
        const view = new vega.View(vega.parse(spec), {
            renderer: 'canvas',
            container: '#view',
            hover: true
        })
        view.addEventListener('click', handleVegaClickEvent)
        view.runAsync()
        setView(view)
    }, [])

    const handleVegaClickEvent = (_: vega.ScenegraphEvent, item?: vega.Item | null) => {
        if (item) {
            setCategory(item.datum.category)
            setAmount(item.datum.amount)
        }
    }

    const handleChangeEvent = (e: ChangeEvent<HTMLInputElement>) => {
        setAmount(parseInt(e.target.value))
    }

    const handleClick = () => {
        if (view) {
            view.change('table', vega.changeset().modify((datum: any) => {
                return datum.category === category
            }, 'amount', amount)).run()
        }
    }

    return (
        <>
            <div id="view"></div>
            <div>
                <div>{category}</div>
                <input type="number" onChange={handleChangeEvent} value={amount}></input>
                <button onClick={handleClick}>Update</button>
            </div>
        </>
    )
}
```


### App.tsx

データを定義し、上で作成したコンポーネントを呼び出しています。

```typescript
import './App.css'
import { BarChart } from './BarChart'

function App() {
  const values = [
      { "category": "A", "amount": 28 },
      { "category": "B", "amount": 55 },
      { "category": "C", "amount": 43 }
  ]
  return (
    <>
      <BarChart values={values} />
    </>
  )
}

export default App
```

上記の実装で以下のように動きます。

![chrome-capture-2023-11-30.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51316/daf971fe-8216-0ee5-dcca-c586e7cc34c7.gif)



### 参考

過去のVega解説記事は以下です。

* [Vega解説 〜離散データ〜](https://qiita.com/ihgs/items/87fda0cd39693bae44f3)
* [Vega解説 〜動的なグラフ〜](https://qiita.com/ihgs/items/7715bd7f6d2f7987f73a)
* [Vega 解説 ~時系列データを処理する~](https://qiita.com/ihgs/items/05e10b9f20faa002be18)
* [Vega解説 〜凡例〜](https://qiita.com/ihgs/items/bb222824f42969317b5d)
* [Vega解説 〜基準線をひく〜](https://qiita.com/ihgs/items/e69927315cb801cb63bc)
* [Vega解説 〜複数グラフ〜](https://qiita.com/ihgs/items/8a54c56929bf67327629)
* [Vega解説 ~全体グラフと詳細グラフ~](https://qiita.com/ihgs/items/73a2cc6b1834d41043e7)