データ可視化への一歩目 〜Angular + Chart.js〜

この記事は [Angular Advent Calendar 2018](https://qiita.com/advent-calendar/2018/angular) の 5 日目の記事です。

# はじめに
IoT や機械学習などデータに触れる機会が多くなったいま、データ収集が積極的に行われています。
当然データを収集するだけでは宝の持ち腐れになってしまいます。
そのデータから

- どの部分を抽出して
- どのように加工して
- どこをどう利用するのか
- どんな知見を得るのか


それによって、持っているデータの価値は大きく変わります。

さらに、得られたデータや知見をどう見せるかという伝え方にも価値があります。
ここではデータをうまく表現する助けとなるように、[Angular](https://angular.io/) と [Chart.js](https://www.chartjs.org/) を使ってデータを可視化する方法を紹介します。

# こんな方へ
- 全体のフレームワークは Angular で、データの可視化ツールを使いたい
- Angular はチュートリアル程度はわかる
- なにかしらのデータを試しに可視化してみたい

## あることないこと
**書いてあること**

- Angular から Chart.js を利用する方法
- 棒グラフを表示する方法
- チャートのクリック・イベントをハンドルする方法
- クリックされたデータの詳細をハンドラから知る方法

下記リンクのページができあがります。
https://charts-sample-2699e.firebaseapp.com/

**書いてないこと**

- Node.js のインストール方法
- Angular CLI のインストール方法
- Angular の文法および書き方

**環境**

- Node.js： 10.13.0
- Angular CLI： 7.0.3

**用語**

[Chart.js Documentation](https://www.chartjs.org/docs/latest/) に習って、グラフではなく `チャート` と書いています。
但し、Bar Chart については `棒グラフ` と書いています。

# 準備
まずは、チャートを表示するための下準備をします。

新しいプロジェクトを作成します。
使用するスタイルシート・フォーマットを聞かれますが、ここでは本質にかかわらないので何でも構いません。

    $ ng new charts-sample --routing

`ng2-charts` と `@types/chart.js` を追加します。

    $ cd charts-sample
    $ yarn add ng2-charts
    $ yarn add @types/chart.js --dev

`src/app/app.modules.ts` で `ChatsModule` を `imports` に記述しておきます。

```typescript:src/app/app.modules.ts
import { ChatsModule } from 'ng2-chats'

@NgModule({
  imports: [
    ...
    ChartsModule
  ],
  ...
})
```

チャート表示用のコンポーネントを作成します。
今回は棒グラフを表示しますのでコンポーネントの名前を `bar-chart` とします。

    $ ng generate component bar-chart

`src/app/app-routing.module.ts` で `BarChartComponent` へのルーティング設定をします。

```typescript:src/app/app-routing.module.ts
import { BarChartComponent } from './bar-chart/bar-chart.component'

const routes: Routes = [
  { path: '**', component: BarChartComponent }
]
```

# チャートの表示
さて、ここからが Angular での Chart.js を使う記述です。

まずは、テンプレートです。
`src/app/bar-chart/bar-chart.component.html` には `canvas` タグを使って各属性を次のように記述しておきます。
`baseChart` 以外の下記値はそれぞれ変数を設定して値が代入されるようにしています。

- chartType
- options
- labels
- legend
- datasets

```html:src/app/bar-chart/bar-chart.component.html
<div>
  <div style="display: block">
    <canvas baseChart
            [chartType]="chartType"
            [options]="chartOptions"
            [labels]="chartLabels"
            [legend]="chartLegend"
            [datasets]="chartData" >
  </div>
</div>
```

`src/app/bar-chart/bar-chart.component.ts` では、先ほどテンプレートで設定した変数を定義します。

棒グラフを表示するので `chatType` の値は `bar` にします。
（他には、折れ線グラフ `line`、円グラフ `pie` などがあります）

`chartOptions` は今回指定していません。

`chartLabels` には月を列挙しています。
これらの値が x 軸のラベルになります。

`chartLegend` を `true` にしておくと、チャートの上に凡例が表示されます。

`chartData` には、表示させたい `data` と `label` をひとつのデータセットとして列挙します。
今回は、東京および京都の降水量データ [^1] を使います。

```typescript:src/app/bar-chart/bar-chart.component.ts
export class BarChartComponent implements OnInit {

  chartType = 'bar'

  chartOptions = { }

  chartLabels = [
    'January', 'February', 'March', 'April', 'May', 'June', 'July',
    'August', 'September', 'October', 'November', 'December' ]

  chartLegend = true

  chartData = [
    { data: [52.3, 56.1, 117.5, 124.5, 137.8, 167.7, 153.5, 168.2, 209.9, 197.8, 92.5, 51.0], label: 'Tokyo' },
    { data: [50.3, 68.3, 113.3, 115.7, 160.8, 214.0, 220.4, 132.1, 176.2, 120.9, 71.3, 48.0], label: 'Kyoto' }
  ]

  constructor() { }
  ngOnInit() { }
}
```

## 実行結果
ビルドしてブラウザで表示すると、下のようなチャートが表示されます。
<img width="1439" alt="Screen Shot 2018-12-03 at 11.20.10.png" src="https://qiita-image-store.s3.amazonaws.com/0/158187/63741c98-147c-7c62-86a1-36b256bb5677.png">

各データセットにはそれぞれ色が付いていて、棒はアニメーションされ、マウスをホバーするとツールチップでデータの詳細が表示されます。
上部に表示されている凡例からデータセットをクリックすることで、選択したデータセットの表示／非表示を切り替えることができます。

# クリック・イベント
ここまでで簡単なチャートの表示ができました。
次は、棒グラフがクリックされたときのイベント・ハンドラを定義する方法です。

クリック・イベントを扱えると、ユーザーのマウス操作でさまざまな表示切り替えができるようになります。

まず、先ほどのテンプレートに `(chartClick)="chartClicked($event)"` という記述を加えます。
これでチャート上でクリック・イベントが起こったときに `chartClicked($event)` が呼ばれます。

```html:src/app/bar-chart/bar-chart.component.html
<div>
  <div style="display: block">
    <canvas baseChart
            [chartType]="barChartType"
            [options]="barChartOptions"
            [labels]="barChartLabels"
            [legend]="barChartLegend"
            [datasets]="barChartData"
            (chartClick)="chartClicked($event)" >
  </div>
</div>
```

クリックされた棒のデータを表示するために、変数を定義しておきます。

```typescript:src/app/bar-chart/bar-chart.component.ts
  clickedLabel        = ''
  clickedDatasetLabel = ''
  clickedValue        = 0

  constructor() { }
```

テンプレートで、定義した変数が表示されるようにしておきます。

```html:src/app/bar-chart/bar-chart.component.html
<div>
  ...
  <div>
    <table>
      <tr>
        <td>Label</td>
        <td>{{clickedLabel}}</td>
      </tr>
      <tr>
        <td>Dataset Label</td>
        <td>{{clickedDatasetLabel}}</td>
      </tr>
      <tr>
        <td>Value</td>
        <td>{{clickedValue}}</td>
      </tr>
    </table>
  </div>
</div>
```

クリック・イベントが起こると `chartClicked` にはイベント・データが渡ってきます。
そこから `model`、`index`、`datasetIndex` を取り出します。
`model` にはクリックされた棒のモデル定義が入っており、x 軸のラベルやデータセットのラベルが含まれています。
`index` はクリックされた棒のデータのインデックスです。
`datasetIndex` はクリックされた棒のデータセットのインデックスです。今回の例では Tokyo データセットが 0、Kyoto データセットが 1 になります。

これらを使って、クリックされた棒のラベル、データセットのラベル、データの値を取得することができます。

```typescript:src/app/bar-chart/bar-chart.component.ts
  // Event Handler
  public chartClicked(e: any): void {
    if (e.active.length > 0) {
      const chart        = e.active[0]._chart
      const element      = chart.getElementAtEvent(event)[0]
      const model        = element._model
      const index        = element._index
      const datasetIndex = element._datasetIndex

      this.clickedLabel        = model.label
      this.clickedDatasetLabel = model.datasetLabel
      this.clickedValue        = chart.config.data.datasets[datasetIndex].data[index]
      }
  }
```

## 実行結果
ビルドしてブラウザで表示すると、下のようなチャートが表示されます。
ひとつの棒をクリックすると、左下の表にクリックした棒の x 軸のラベル、データセットのラベル、およびデータ値が表示されます。
<img width="1349" alt="Screen Shot 2018-12-03 at 18.26.00.png" src="https://qiita-image-store.s3.amazonaws.com/0/158187/87901297-0f47-d93c-b0db-68806fa9faaa.png">

# まとめ
Angular から可視化ツールである Chart.js を使って、棒グラフを表示する手順を紹介しました。
これを参考にすることで、他の `chartType` でも [Chart.js Charts](https://www.chartjs.org/docs/latest/charts/) を参照することで簡単に扱えるはずです。

棒グラフの棒をクリックしたときに発生するクリック・イベントをハンドルして、各種値を取得する方法を紹介しました。
ユーザーの操作によって、ページやチャートを変化させることができます。

こちらで動きを確認できます。
https://charts-sample-2699e.firebaseapp.com/

明日の 6 日目は @miyatomo さんです。

# おまけ （JSON データ）
データを JSON ファイルから読み込みたいときには、`tsconfig.json` で下記の設定をしておきます。
こうすることで、JSON ファイルをモジュールのように直接インポートすることができます。

```json:tsconfig.json
   "resolveJsonModule": true,
   "allowSyntheticDefaultImports": true
```

例えば、下記のように記述すれば直接インポートすることができます。

```typescript
import datajson from '../../data/opendata01.json'
```

# おまけ （ホバー・イベント）
マウス・ホバーのイベントは、クリック・イベント同様にテンプレートに `(chartHover)="chartHovered($event)"` のように記述することでハンドラが呼ばれます。
しかし、不具合があるようで現在 (2018/12/4) はうまく動かずハンドラが呼ばれません [^2]。

```html:src/app/bar-chart/bar-chart.component.html
<div>
  <div style="display: block">
    <canvas baseChart
            [chartType]="chartType"
            [options]="chartOptions"
            [labels]="chartLabels"
            [legend]="chartLegend"
            [datasets]="chartData"
            (chartHover)="chartHovered($event)" >
  </div>
</div>
```

テンプレートではなく `chartOptions` に、`onHover` を持つオブジェクトを `hover` として定義します。
こう書くことでホバー・イベントで `onHover` ハンドラが呼ばれます。

```typescript:src/app/bar-chart/bar-chart.component.ts
export class BarChartComponent implements OnInit {

  chartType = 'bar'

  chartOptions = {
    hover: {
      onHover: (event, active) => {
        if (active && active.length) {
          // Your code is here.
        }
      }
    }
  }
```

# リファレンス
[Chart.js](https://www.chartjs.org/)
[Chart.js Documentation](https://www.chartjs.org/docs/latest/)
[ng2-charts](https://valor-software.com/ng2-charts/)
[Angular & Chart.js (with ng2-charts)](https://medium.com/codingthesmartway-com-blog/angular-chart-js-with-ng2-charts-e21c8262777f)

[^1]: 使用した降水量データ：[東京都 東京 の気候](https://weather.time-j.net/climate/chart/tokyo)、[京都府 京都 の気候](https://weather.time-j.net/Climate/Chart/Kyoto)
[^2]: [chartHover event is not working #842](https://github.com/valor-software/ng2-charts/issues/842)