Vue3とvue-chartjsでレーダーチャートの実装を試してみる

Vue3でレーダーチャートを実装する必要が出てきたため、手元で動かしてみるために必要そうな情報をまとめます。
使用するライブラリとしては、差し当たりGitHubのスター数が多かった`vue-chartjs`を対象としています。


https://github.com/apertureless/vue-chartjs


## 検証環境の準備

`create-vue`コマンドで初期化します。

検証用かつJavaScriptでの実装を想定しているため、プラグインの追加は最小限で良いものとします。

```
❯ npm init vue@latest

Vue.js - The Progressive JavaScript Framework

✔ Project name: … vue3-radar-chart
✔ Add TypeScript? … No
✔ Add JSX Support? … No
✔ Add Vue Router for Single Page Application development? … No
✔ Add Pinia for state management? … No
✔ Add Vitest for Unit Testing? … No
✔ Add Cypress for both Unit and End-to-End testing? … No
✔ Add ESLint for code quality? … Yes
✔ Add Prettier for code formatting? … Yes
```

vue-chartjsのリファレンスのinstallationより必要なパッケージをインストールします。

https://vue-chartjs.org/guide/#installation

```
npm i vue-chartjs chart.js
```

この時点で追加されたパッケージのバージョンは以下の通り。

```package.json
{
  "name": "vue3-radar-chart",
  "version": "0.0.0",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview --port 4173",
    "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
  },
  "dependencies": {
    "chart.js": "^3.9.1",
    "vue": "^3.2.38",
    "vue-chartjs": "^4.1.2"
  },
  "devDependencies": {
    "@rushstack/eslint-patch": "^1.1.4",
    "@vitejs/plugin-vue": "^3.0.3",
    "@vue/eslint-config-prettier": "^7.0.0",
    "eslint": "^8.22.0",
    "eslint-plugin-vue": "^9.3.0",
    "prettier": "^2.7.1",
    "vite": "^3.0.9"
  }
}
```


## レーダーチャートの実装

vue-chartjsのリファレンスの《Examples》にある[実装例](https://codesandbox.io/s/github/apertureless/vue-chartjs/tree/main/sandboxes/radar)を参考にします。

https://vue-chartjs.org/examples/

特に必要性はないですが、単一ファイルコンポーネントへの書き換えを行ってみます。JSファイルとして管理したほうが良いのでしょうか。

サンプルはTypeScriptで実装されていますが、今回はJavaScriptでの実装を想定しているため型情報を落とします。

```App.vue
<script setup>
import RadarChart from './components/RadarChart.vue'
</script>

<template>
  <RadarChart 
    v-bind:chartId="'my-radar-chart'"
    v-bind:cssClasses="'my-radar-chart-container'" />
</template>

<style>
/* assets/main.cssは全行コメントアウト */
.my-radar-chart-container {
  margin: 0 auto;
  max-width: 500px;
}
</style>
```

```components/RadarChart.vue
<script>
import { defineComponent, h } from 'vue'

import { Radar } from 'vue-chartjs'
import {
  Chart as ChartJS,
  Title,
  Tooltip,
  Legend,
  PointElement,
  RadialLinearScale,
  LineElement
} from 'chart.js'

ChartJS.register(
  Title,
  Tooltip,
  Legend,
  PointElement,
  RadialLinearScale,
  LineElement
)

export default defineComponent({
  name: 'RadarChart',
  components: {
    Radar
  },
  props: {
    chartId: {
      default: 'radar-chart'
    },
    width: {
      default: 400
    },
    height: {
      default: 400
    },
    cssClasses: {
      type: String
    },
    styles: {
      default: () => {}
    },
    plugins: {
      default: () => []
    }
  },
  setup(props) {
    const chartData = {
      labels: [
        'Eating',
        'Drinking',
        'Sleeping',
        'Designing',
        'Coding',
        'Cycling',
        'Running'
      ],
      datasets: [
        {
          label: 'My First dataset',
          backgroundColor: 'rgba(179,181,198,0.2)',
          borderCapStyle: 'square',
          borderColor: 'rgba(179,181,198,1)',
          pointBackgroundColor: 'rgba(179,181,198,1)',
          pointBorderColor: '#fff',
          pointHoverBackgroundColor: '#fff',
          pointHoverBorderColor: 'rgba(179,181,198,1)',
          data: [65, 59, 90, 81, 56, 55, 40]
        },
        {
          label: 'My Second dataset',
          backgroundColor: 'rgba(255,99,132,0.2)',
          borderColor: 'rgba(255,99,132,1)',
          pointBackgroundColor: 'rgba(255,99,132,1)',
          pointBorderColor: '#fff',
          pointHoverBackgroundColor: '#fff',
          pointHoverBorderColor: 'rgba(255,99,132,1)',
          data: [28, 48, 40, 19, 96, 27, 100]
        }
      ]
    }

    const chartOptions = {
      responsive: true,
      maintainAspectRatio: false
    }

    return () =>
      h(Radar, {
        chartData,
        chartOptions,
        chartId: props.chartId,
        width: props.width,
        height: props.height,
        cssClasses: props.cssClasses,
        styles: props.styles,
        plugins: props.plugins
      })
  }
})
</script>

```

基本的にはvue-chartjsからインポートした`Radar`コンポーネントを第1引数に、`props` 、`chartData` 、`chartOptions` をオブジェクトリテラルでまとめたものを第2引数として Vueの `h() 関数` に渡し、さらにそれを`Render関数`に渡して描画させるという構造のようです。

- [Render関数のシグネチャ](https://v3.ja.vuejs.org/guide/migration/render-function-api.html#_3-x-%E3%81%A6%E3%82%99%E3%81%AE%E6%A7%8B%E6%96%87-2)

- [h() 関数のリファレンス](https://v3.ja.vuejs.org/guide/render-function.html#h-%E3%81%AE%E5%BC%95%E6%95%B0)

```
return () =>
  h(Radar, {
    chartData,
    chartOptions,
    chartId: props.chartId,
    width: props.width,
    height: props.height,
    cssClasses: props.cssClasses,
    styles: props.styles,
    plugins: props.plugins
  })
```

詳細の確認は省略しますが、h()関数の第2引数にはvue-chartjsのRadarコンポーネントに定義されたプロパティを渡すという理解で良さそうです。

また、余談としてRadarChart.vueも`<script setup>`の記法で書きたかったのですが、以下の通りRender関数との相性が悪いようでした。

https://v3.ja.vuejs.org/api/sfc-script-setup.html#%E9%80%9A%E5%B8%B8%E3%81%AE-script-%E3%81%A8%E3%81%AE%E4%BD%B5%E7%94%A8

> このケースでは render 関数はサポートされていません。代わりに、通常の \<script\> と setup オプションを使用してください。


以下に各プロパティ設定値の定義についての参照先を記載します。

### chartId / width / height / cssClasses / styles

グラフが実際に描画される際には\<div\>タグ、\<canvas\>タグの入れ子になるようです。開発者ツールで確認することができます。

そしてこれらの設定値は以下のvue-chartjsの実装から、それぞれのタグに対する属性指定であると見えます。

[vue-chartjs/src/BaseCharts.ts L263-271](https://github.com/apertureless/vue-chartjs/blob/386c8f1ddbb614ed50710c1eb56c917ac326c5bc/src/BaseCharts.ts#L263)
```
return () =>
  h('div', { style: props.styles, class: props.cssClasses }, [
    h('canvas', {
      id: props.chartId,
      width: props.width,
      height: props.height,
      ref: canvasEl
    })
  ])
```

### chartData

データ構造は`Chart.js`リファレンスの`Radar Chart`の《Dataset Properties》の記載に準拠するようです。

Namespace: dataに対応。

以下のリファレンスを確認します。

https://www.chartjs.org/docs/latest/charts/radar.html#dataset-properties

### chartOptions

データ構造は`Chart.js`リファレンスの《Configuration》配下の記載に準拠するようです。

Namespace: optionsに対応。

差し当たり、`responsive`、`maintainAspectRatio`については以下のリファレンスを確認します。

https://www.chartjs.org/docs/latest/configuration/responsive.html#configuration-options

## Appendix

このままの実装だとChart.jsのRadar Chartの[サンプル](https://www.chartjs.org/docs/latest/charts/radar.html#radar-chart)の見た目と異なり、線で囲われたエリアの背景色が付かない点が気になります。

関連情報を調べたところ、Chart.jsから`Filler`モジュールをインポートすれば背景色が付くことがわかりました。

https://stackoverflow.com/questions/70073007/chartjs-backgroundcolor-for-line-chart-does-not-appear-in-vue-app

リファレンスとしてはこちらに記載があるようです。

https://www.chartjs.org/docs/latest/charts/area.html#area-chart

```
import {
  Chart as ChartJS,
  Title,
  Tooltip,
  Legend,
  PointElement,
  RadialLinearScale,
  LineElement,
  Filler // 追加
} from 'chart.js'

ChartJS.register(
  Title,
  Tooltip,
  Legend,
  PointElement,
  RadialLinearScale,
  LineElement,
  Filler // 追加
)
```

![vue3-radar-chart.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105368/4a848515-dd6c-ad2b-f819-87e7d44683ad.png)

インポートして登録するだけでで有効になるようですが、`fill`プロパティで明示化したほうが良いかもしれません。