【JavaScript】チャートの指標を動的に切り替える【chart.js】

## はじめに
`chart.js` 便利ですよね。
JSのライブラリ全体でもトップクラスにGitHubのスター数が多く、2023/03/05現在でも最終更新が2日前にされているなどだいぶ活発なライブラリなので、JSを触っていて使ったことがある方も少なくないかと思います。

たとえば、月別の売上データみたいなものをチャートで表現するというタスクがあったとします。
なおかつ、それを（デザイン等の都合で）1つのチャートで何かしらのアクションによって動的に切り替えたいという要件が有り得るかと思います。 ~~（私はあった）~~

そんな実装を過去に行ったことがあったので、今回はそちらを紹介 ~~もとい過去の遺産を公開~~ したいと思います。
とりあえず、chart.jsについて…みたいなのは省略。

## いったんコードを貼ってみる
<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="wvEerjM" data-user="tsuchinoko0102" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/tsuchinoko0102/pen/wvEerjM">
  Untitled</a> by Tsuchinoko0102 (<a href="https://codepen.io/tsuchinoko0102">@tsuchinoko0102</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

6/1〜6/7まで７日間の、降水量・気温・降水確率・雨量の4つの指標をチャート化した、という事例にしてみました。（データは適当です）

HTMLとJSのみ実装してます。 ~~CSSめんどくせぇんだもん~~
HTMLの方は最低限の実装しかせず特筆すべき点はないので割愛。以下、JSの実装内容について詳細解説します。

2023/03/05時点での最新版は`ver4.2.1`ですが、なぜかCodePenでver4系が動いてくれなかったので`ver3.5.1`で実装を行いました。今回紹介する実装に関しては3系も4系も変わらない（はず）なので、特に問題はないと思われます。

私jQueryおじさんで、バニラを書くのがとても久しぶり（というかほとんど書いたことない）ので、汚いとか、こう書き換えた方がモダンだ！みたいなことがあれば、是非ご教示いただきたく思います。

## 実装した中身
### ChartPropsクラスを作る
1~50行目まで。
<details><summary>ChartPropsクラスの作成と初期化</summary>

``` javascript
// チャートの指標に関するプロパティをクラスで宣言
class ChartProps {
  name = '';      // チャート上部に表示される指標名
  titleText = ''; // 縦軸の外側に表示する「単位」
  chartData = []; // 実際にチャートに描画されるデータ
  
  set name(_name) {
    this.name = _name;
  }
  
  set titleText(txt) {
    this.titleText = txt;
  }
  
  set chartData(data) {
    this.chartData = data;
  }
  
  get name() {
    return this.name;
  }
  
  get titleText() {
    return this.titleText;
  }
  
  get chartData() {
    return this.chartData;
  }
}

const rainFall = new ChartProps();
rainFall.name = '雨量';
rainFall.titleText = 'mm';
rainFall.chartData = [0, 0, 3, 10, 0, 5, 8];

const temperature = new ChartProps();
temperature.name = '気温';
temperature.titleText = '℃';
temperature.chartData = [20, 22, 18, 16, 20, 18, 17];

const rainyPercent = new ChartProps();
rainyPercent.name = '降水確率';
rainyPercent.titleText = '%';
rainyPercent.chartData = [0, 0, 10, 30, 0, 10, 20];

const humidity = new ChartProps();
humidity.name = '湿度';
humidity.titleText = '%';
humidity.chartData = [40, 45, 70, 90, 50, 70, 80];
```
</details>
今回、クラスを書いたことがなかったので勉強がてらクラスを使って処理を行いましたが、別にクラス宣言である必要はありません。
オブジェクトにして、各関数の処理をそれに即したものに変えれば十分です。

<details><summary>こんな感じで</summary>

```javascript
const mixedChartObj = {
    'rainFall': {
        'name': '雨量',
        'titleText': 'mm'
        'data': [0, 0, 3, 10, 0, 5, 8],
    },
    // 以下略
```
</details>

ただ、実際の運用ではデータはajaxやバックエンドから取得してくるケースがほとんどだと思いますので、実用性まで兼ねると以下のような形がいいのかななんて思います。

```javascript
class ChartProps {
  constructor(name, titleText) {
    this.name = name;
    this.titleText = titleText;
  }
  
  set chartData(data) {
    this.chartData = data;
  }
  get chartData() {
    return this.chartData;
  }
}
```

### その他チャート生成に必要な要素を作っておく
52~89行目まで。
<details><summary>ラベルとオプション</summary>

```javascript
// チャート下部
const chartLabel = ['6/1', '6/2', '6/3', '6/4', '6/5', '6/6', '6/7'];

// オプションは指標変更ごとに変わる項目があるので関数化
const option = function chartOption(data1, data2) {
  return {
    scales: {
      'y-axis-1': {
        type: 'linear',
        position: 'left',
        ticks: {
          max: Math.max(...data1.chartData),
          min: 0,
        },
        title: {
          display: true,
          text: data1.titleText,
        }
      },
      'y-axis-2': {
        type: 'linear',
        position: 'right',
        ticks: {
          max: Math.max(...data2.chartData),
          min: 0,
        },
        title: {
          display: true,
          text: data2.titleText,
        },
        grid: {
          drawOnChartArea: false,
        },
      },
    },
    responsive: false,
  }
};
```
</details>

仮に、6月1日〜6月7日までのデータを表示したかったので、 `chartLabel`で予めセット。
これも、Ajaxやバックエンドから取得する度に変わる（たとえば月ごとのデータとか）の場合は、描画するデータによって動的に変える処理を入れて上げる必要があります。

~~あまり書くことがなさそうなので~~ チャートのオプション周りについても、今回使っているものに関しては解説します。
chart.jsのオプション周りってリファレンス読みにくいのよね。

```javascript:都合によりy-axis-1は消したもの
scales: {
  'y-axis-2': {
    type: 'linear',
    position: 'right',
    ticks: {
      max: Math.max(...data2.chartData),
      min: 0,
    },
    title: {
      display: true,
      text: data2.titleText,
    },
    grid: {
      drawOnChartArea: false,
    },
  },
  responsive: false,
}
```
`scales`オプションは、ざっくりいうと目盛だったり横軸だったり見た目に関わるところのオプション群です。
`scales`オプションの中に更に階層が掘られていて、各種設定が行えます。

`y-axis-2`これは、後にchartを初期化するところで`yAxisID`というプロパティに差すIDです。名前は自由のはず。
今回、棒グラフと折れ線グラフ2つのチャートを合わせた`Mixed Chart Type`というチャートを作っており、チャートを生成するための`datasets`を配列で２つ格納しています。そして、その2つ目（=折れ線グラフ）側の設定を`y-axis-2`としています。

`type`：目盛の表現のされかた。今回みたいに数字を扱う系のチャートなら大体`linear`にしておけば間違いない。
`position`：目盛を左右どちらに表示するか。上に遡ってチャートをご覧いただくと、折れ線グラフの目盛が右側に表示されているのが分かります。
`ticks`：目盛数字の設定。今回は、データの最大値をチャートの頂点に持って行きたかったので`Math.max`で計算してます。
`title`：単位。mmや%が表示されていますが、それの設定です。必要なければ`display:false`にして消しましょう。
`grid`：横軸の表示に関する設定です。MixedChartで２つの指標両方の横軸が表示されると見た目が汚いので、`drawOnChartArea: false`で折れ線グラフ側の横線は表示しないようにしました。

`responsive`：trueにすると、レスポンシブデザインによしなに対応してくれます ~~が、うざいのでfalseにしました~~。

### チャートの初期化
91〜118行目まで。
<details><summary>chartの初期化</summary>

```javascript
// chart初期化のお約束
const ctx = document.getElementById('mixedChart');
const myChart = new Chart(ctx, {
  data: {
    datasets: [{
      type: 'bar',
      label: rainFall.name,
      data: rainFall.chartData,
      backgroundColor: 'rgba(66, 133, 244, 1)',
      borderColor: 'rgba(66, 133, 244, 1)',
      order: 2,
      borderWidth: 1,
      yAxisID: 'y-axis-1',
    }, {
      type: 'line',
      label: rainyPercent.name,
      data: rainyPercent.chartData,
      backgroundColor: 'rgba(84, 84, 84, 1)',
      borderColor: 'rgba(84, 84, 84, 1)',
      order: 1,
      borderWidth: 1,
      yAxisID: 'y-axis-2',
    }],
    labels: chartLabel,
  },
  // optionsは都度変更があるので関数でセット
  options: option(rainFall, rainyPercent),
  });
```
</details>

基本的なchart.jsでのチャート初期化式なので特筆すべき点はほぼありません。強いて言えば`order`オプションくらいでしょうか。２つの指標のどちらを前に持ってくるかを決めるオプションです。
色味の問題もありますが、orderを逆にすると折れ線グラフが埋没するので折れ線グラフの方を`order:1`としています。

### ようやく本番、動的に切り替える処理
120〜146行目（最後）まで。
<details><summary>セレクトボックスでチャート内容を変更する</summary>

```javascript
// セレクトボックスの変更によってチャートを変更する処理
const select = document.getElementById('selectBoxContainer');

select.addEventListener('change', () => {
  const barVal = select.children[0].value;
  const lineVal = select.children[1].value;
  const datasets = myChart.data.datasets;
  
  /* イベント時に選択されているセレクトボックスのvalueで
  データをセットし直すChartPropsを切り替える */
  const data1 = barVal === 'temperature' ? temperature : rainFall;
  const data2 = lineVal === 'humidity' ? humidity : rainyPercent;
  
  // myChartのオブジェクトを上書き
  datasets[0].data = data1.chartData;
  datasets[0].label = data1.name;
  datasets[0].titleText = data1.titleText;

  datasets[1].data = data2.chartData;
  datasets[1].label = data2.name;
  datasets[1].titleText = data2.titleText;
  
  myChart.options = option(data1, data2);
  
  // .update()メソッドによって初めてチャートが更新される
  myChart.update();
});
```
</details>

チャートに何かしらの変更を加えるときは、 **一度作ったチャートを更新する** という処理が正着になります。
~~（まぁ別に毎回destroyして、再度newしてもいいのかもしれないけど）~~
そのため、チャート初期化時に`myChart`という変数でチャートオブジェクトを保持、それを後々使い回すという手法を取りました。

棒/折れ線それぞれに用意した2つのセレクトボックスのどちらかが変更されると、そのとき選択されている項目のインスタンスを使ってチャートオブジェクトの変更したいプロパティに変更を加えてチャートを上書きするという処理を行っています。

チャートの更新は、チャートオブジェクトの書き換えだけでなく、最後に必ず`.update()`メソッドを使う必要があるので要注意。


今回は、サンプルとしてセレクトボックスによるチャートの更新を行いましたが、たとえば、◀ ▶ボタンを置いて年月を変更する度にajaxを走らせてデータを取りに行って更新する、なんてこともあるかもしれないですが、やることは同じです。
チャートオブジェクトの`datasets`の中身を上書きして`update()`で更新！簡単です。

## 最後に
かれこれ10ヶ月程前、初めて触るchart.jsがこんな具合の案件だったので、とても苦労したことを思い出しながら書きました。

ChartPropsクラスを作る処理のところにも書いた通り、（今に至るまで）JSでクラスを書いたことがなく案件で実装を行った当時は「こんな感じ」オブジェクトを作って、変更のときはオブジェクトを`for ~ in`でループさせて、その中をさらにループさせて…みたいな書き方をしたこと。
chart.jsのリファレンス（特にオプション周り）が個人的にはめちゃくちゃ分かりづらいものだったこと。
しかも必要なチャートがMixedChartだけでなくDoughnutChart（円グラフ）もあり、DoughnutChartの方は、もう少し複雑なロジックでのチャート更新が求められたこと。
これらが要因で非常に苦労したし工数も半端なく掛かったような記憶があります。

10ヶ月経ち、改めて触ってみた感想としては、**クラス使えばよかった…** の一言に尽きます。マジで。
本題はあくまで`chart.js`ですが、今回の私の学びとしては圧倒的に`クラスが分かってよかった`の方でした。

## 参考リンク
当時いろいろ参考にしたはずなんだけど、全然覚えていないので今回は省略させていただきます。
10ヶ月前の私に知恵を授けてくださった先人様方、どうもありがとうございました。

Chart.js公式
https://www.chartjs.org/