vue-chart.jsで複数ラベルを表示させる

## はじめに
　vue-chartjsを使って、下図のように折れ線グラフの横軸ラベルを複数表示する方法について書きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253933/cf54a545-99c6-0290-b4cc-839f1a608837.png)



なお、本記事にはその他に以下のような情報を含みます。

* tooltip<sup>※1</sup>の表示内容をカスタマイズする方法
* グラフエリアの背景色を設定する方法

　※1 データ点にカーソルを合わせると出てくる吹き出し

## 前提

### 実行環境

* m1 mac
* node        14.15.1
* vue/cli     4.5.9
* vue ^2.6.11
* chart.js    ^2.9.4
* vue-chartjs ^3.5.1

　本記事投稿時点では、[vue-chartjsがchartjsの3系に対応していなかったり](https://github.com/apertureless/vue-chartjs/issues/695) で前提環境が若干繊細なようなのでその点はご注意ください。

### ディレクトリ構成(抜粋)

```bash
├── src
│   ├── components
│   │    └── LineChart.vue # 折れ線グラフコンポーネント
│   └── pages
│        └── Home.vue # グラフ表示ページ
```
　Home.vueの中で、LineChart.vueをインポートしてグラフを表示させます。なお、ルーティングなどについては記載を省略します。

## 複数ラベル表示の実装

　具体的な実装についてです。まず、LineChart.vueについてですがこちらは[公式](https://vue-chartjs.org/ja/guide/#%E3%83%81%E3%83%A3%E3%83%BC%E3%83%88%E3%83%87%E3%83%BC%E3%82%BF%E3%81%AE%E6%9B%B4%E6%96%B0)のものをほとんど流用した形で以下のようにします。

```HTML:LineChart.vue
<script>
import { Line, mixins } from 'vue-chartjs'
const { reactiveProp } = mixins

export default {
  extends: Line,
  mixins: [reactiveProp],
  name: 'line-chart',
  props: ['chartData', 'options'],
  mounted () {
    this.renderChart(this.chartData, this.options)
  }
}
</script>
```

　次に、本題のHome.vueについてです。ラベルを複数表示するための基本的な方針は以下の通りです。

> 1. データ定義 (`chartData`)  
 1.1　`labels`に複数のラベルをまとめて格納  
 1.2　`datasets`内のデータに`xAxisID`を定義  
> 2. オプション定義 (`options`)  
 2.1　`xAxes`内に、表示したいラベルパターン分の軸を定義   
 2.2　軸を定義する際に、いずれかの軸のidにデータ定義の`xAxisID`につけた名前と同じ名前をつける [^1]   
 2.3　1.1のまとめ方に合わせて、 [ticksのcallback関数](https://www.chartjs.org/docs/latest/axes/labelling.html) でラベル情報を取り出す  

[^1]: どれか一つに一致させていれば問題なく表示されます。

2.3の「まとめ方」によって実装方法が変わるので、そのパターンと実装例を以下に記します。

### パターン1. ラベルを合体させる

　以下の`labels`のように、`2020`というラベルと`マヂカルラブリー`というラベルを`:`でくっつけて一つの複合ラベルとして定義します。そして、 `options > scales > xAxes` 内の `ticks`のcallback関数にて、前述の複合ラベルを`:`で分割し、取り出したい箇所をreturnしています。

```HTML:Home.vue
<template>
  <div class="small">
    <line-chart
      :chart-data="chartData"
      :options="options"
      :width="width"
      :height="height"></line-chart>
  </div>
</template>

<script>
import LineChart from '../components/LineChart'
export default {
  components: { LineChart },
  data () {
    return {
      chartData: {
        labels: [
          '2015:トレンディエンジェル',
          '2016:銀シャリ',
          '2017:とろサーモン',
          '2018:霜降り明星',
          '2019:ミルクボーイ',
          '2020:マヂカルラブリー'
        ],
        datasets: [
          {
            label: '優勝者の得点',
            xAxisID: 'year',
            backgroundColor: 'rgba(250,45,6,0.5)', //点の色を指定
            borderColor: 'rgba(44,33,0,0.5)', //折れ線の色を指定
            data: [825, 470, 645, 662, 681, 649],
            fill: false, //折れ線の下は塗らない
            lineTension: 0, //点間の線は直線にする
            pointRadius: 8 //データ点のサイズを指定
          }
        ]
      },
      options: {
        scales: {
          xAxes: [
            {
              id: 'year',
              type: 'category',
              scaleLabel: {
                display: false, //「開催年」を軸に表示させたかったらtrueに
                labelString: '開催年'
              },
              ticks: {
                callback: function(label){
                  return label.split(":")[0]; //":"で分割した1つ目を"開催年"として抽出
                }
              }
            },
            {
              id: 'winner',
              type: 'category',
              scaleLabel: {
                display: false, //「優勝者」を軸に表示させたかったらtrueに
                labelString: '優勝者'
              },
              ticks: {
                callback: function(label){
                  return label.split(":")[1].replace("ー", "|").split(""); //縦書き表示させるためのsplitとその際に横棒を縦棒で表示させるためのreplace
                },
              },
              gridLines: {
                display: false // 軸のグリッド線を非表示に
              }
            }
          ],
          yAxes: [{
            scaleLabel: {
              display: true,
              labelString: '得点'
            },
            ticks: {
              beginAtZero: true,
              stepSize: 100,
            }
          }]
        }
    },
    width: 550,
    height: 400
    }
  }
}
</script>
```

* メリット
 * 実装がとても簡単
* デメリット
 * 区切りに使う文字(今回だと`:`)がラベルの中に含まれてしまうとうまくいかなくなってしまう

　区切り文字に注意すれば普通に使える方法だと思いますが、それだったらもっと安全な方法をという場合はパターン2以降の方法が良いと思います。


### パターン2. ラベルを２次元配列にする
　パターン1の区切り文字問題を回避するためにlabelを2次元配列にして格納する方法です。
この場合、以下のようにラベルを2次元配列で格納しておき、

```Javascript:Home.vue(抜粋)
      chartData: {
        labels: [
          ['2015', 'トレンディエンジェル'],
          ['2016', '銀シャリ'],
          ['2017', 'とろサーモン'],
          ['2018', '霜降り明星'],
          ['2019', 'ミルクボーイ'],
          ['2020', 'マヂカルラブリー']
        ],
```

ticksのcallback関数にて、配列の要素をそれぞれ取得することでラベルを別々に扱うことができます。

```Javascript:Home.vue(抜粋)
          xAxes: [
            {
              id: 'year',
              type: 'category',
              scaleLabel: {
                display: false,
                labelString: '開催年'
              },
              ticks: {
                callback: function(label){
                  return label[0];
                }
              }
            },
            {
              id: 'winner',
              type: 'category',
              scaleLabel: {
                display: false,
                labelString: '優勝者'
              },
              ticks: {
                callback: function(label){
                  return label[1].replace("ー", "|").split("");
                }
              },
              gridLines: {
                display: false
              }
            }
          ],
```

* メリット
 * 実装が比較的簡単
 * 区切り文字を気にしなくてよくなる

* デメリット
 * tooltipの表示が若干イケてない
     * この方法だと、tooltipの表示が以下の右側のようになります。そこまで違和感はないですが、左側のパターン1の時よりも若干表示がイケてなく感じました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253933/1bd41129-d539-bb93-a553-c04bc551535a.png" width="200px"><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253933/02465683-6cdd-6676-3566-a7b5a7e83b43.png" width="200px">

### パターン3. ラベルをオブジェクトにする

　パターン2の実装方法で基本的には対応可能ですが、どうせなら配列じゃなくてオブジェクトにしたいなと思ったので、その方法について最後に示します。

　まず、下記のように`labels`をオブジェクトで定義します。


```Javascript:Home.vue(抜粋)
      chartData: {
        labels: [
          { year: 2015, winner: 'トレンディエンジェル'},
          { year: 2016, winner: '銀シャリ'},
          { year: 2017, winner: 'とろサーモン'},
          { year: 2018, winner: '霜降り明星'},
          { year: 2019, winner: 'ミルクボーイ'},
          { year: 2020, winner: 'マヂカルラブリー'}
        ],
```

続いて、ticksのcallback関数で以下のように対応するプロパティのみ返すようにします

```Javascript:Home.vue(抜粋)
          xAxes: [
            {
              id: 'year',
              type: 'category',
              scaleLabel: {
                display: false,
                labelString: '開催年'
              },
              ticks: {
                callback: function(label){
                  return label.year;
                }
              }
            },
            {
              id: 'winner',
              type: 'category',
              scaleLabel: {
                display: false,
                labelString: '優勝者'
              },
              ticks: {
                callback: function(label){
                  return label.winner.replace("ー", "|").split("");
                },
              },
              gridLines: {
                display: false
              }
            }
          ],
```

　これでやりたいことは実現できたのですが、`labels` をオブジェクトにしたことで、tooltipが以下のようにおかしな表示になってしまうという弊害が生じてしまいました。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253933/7bc1d343-c3fb-ad45-418a-c00b29929a56.png" width="200px" />

　そこで、次にこれを解消する2つの方法について次に記します。

#### 方法1. tooltipを非表示にする
　tooltipがなくても別にいいし楽な方がいいという場合はこの方法が楽だと思います。これは`options`に以下のように追記することで実現できます。

```Javascript:Home.vue(抜粋)
      options: {
        //
        //途中省略
        //
        tooltips: {
          enabled: false
        },
```

#### 方法2. tooltipをカスタマイズする
　こちらは、tooltipの表示をカスタマイズすることでちゃんと表示されるようにする方法です。試しに今回は以下のように表示されるようにカスタマイズしてみます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253933/52a1a2cc-2464-7e41-ae46-5533c224c17a.png" width="200px">


　これは、`options`に`tooltips`を追加し、以下のようなcallback関数を定義することで実現できます。`label`が下に表示される文字、`title`が上に表示される文字になります。

```Javascript:Home.vue(抜粋)
      options: {
        //
        //途中省略
        //
        tooltips: {
          callbacks: {
            label: function(tooltipItems, data) {
              const labelText = `${data.datasets[tooltipItems.datasetIndex].label}: ${data.datasets[0].data[tooltipItems.index]} 点`;
              return labelText;
            },
            title: function(tooltipItems) {
              const labelText = `${tooltipItems[0].xLabel.year} ${tooltipItems[0].xLabel.winner} 優勝
              `;
              return labelText;
            }
          }
        },
```

パターン3のメリット・デメリットを記載しておきます。

* メリット
 * コードの可読性が高くなり、`chartData`オブジェクトの使い回しがしやすい
* デメリット
 * コード量が若干多くなる

　`chartData`の`labels`をオブジェクトにしておくと、`chartData`を別途テーブル表示やCSV出力したいときに実装が楽になるかなと思います。

## ついでに

　`はじめに`で載せたグラフは、グラフエリアの背景に色がついていますが、そのやり方を説明していないのでついでに記載しておきます。(単純に`options`で背景色を設定するとグラフ全体に背景色がつきます)

方法としては、まずLineChart.vueに以下を追加します。

```Javascript:LineChart.vue(抜粋)
  mounted () {
    this.addPlugin({
      beforeDraw: function (chart) {
        if (chart.config.options.chartArea && chart.config.options.chartArea.backgroundColor) {
          const ctx = chart.chart.ctx;
          const chartArea = chart.chartArea;

          ctx.save();
          ctx.fillStyle = chart.config.options.chartArea.backgroundColor;
          ctx.fillRect(chartArea.left, chartArea.top, chartArea.right - chartArea.left, chartArea.bottom - chartArea.top);
          ctx.restore();
        }
      }
    }),
    this.renderChart(this.chartData, this.options)
  }
```

これによって、`options`の`chartArea`でグラフエリアに背景色を設定できるようになるので、Home.vueのの`options`に以下のように追記することで好きな背景色をつけることができます。

```Javascript:Home.vue(抜粋)
        chartArea: {
            backgroundColor: 'rgba(32, 232, 255, 0.1)'
        },
```




## おわりに
　今回の記事を応用すれば、[ラベルを階層表示](https://stackoverflow.com/questions/42934608/how-to-create-two-x-axes-label-using-chart-js
)したりもできそうです。このあたりはなかなか手探りになりがちなので、標準で細かくカスタマイズできるようになると嬉しい限りです。

ちなみに私はマヂカルラブリーは好きではありません。