初心者のVuetify | Vuetify + Chart.jsで、グラフカードコンポーネントの作成

<img width="100%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152224/267307f0-0832-7fd7-df87-c22dbd84a3db.png" />

ダッシュボードを作成するのに、カード型のUIでグラフ表示をしてみたかったので、 
VuetifyとChart.jsで簡単な実装をしてみました。

# 対象読者
以下の内容を書いてます。

* Vuetifyのカードコンポーネント実装
* そこに、Chart.jsの埋め込み
* Nuxt.JSのレンダリングタイミングの制御

# できたもの
![result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152224/b00ec3d1-a9d4-8efb-2583-0113b10f1886.png)


# 準備

`create-nuxt-app`でプロジェクトを作成します。  
 **参考** [インストール - NuxtJS](https://ja.nuxtjs.org/docs/2.x/get-started/installation/#create-nuxt-app-%E3%82%92%E4%BD%BF%E3%81%86)

```bash
$ yarn create nuxt-app <project-name>
```

もしくは、GitHubにプロジェクトソースを置いてるので、それを利用してください。 
今回は、Vuetifyベースでの記述になります。

Nuxt.JSでChart.jsを利用するために、Chart.jsと、vue-chart.jsをインストールします。

```bash
yarn add vue-chartjs chart.js
```

# データをベタガキ
まずは、Chart.jsの設定に直接値を書き込みます。  
`components/charts/chart-card.vue`

## グラフ部分作成
まず,Chart.jsのレンダリングをするコンポーネントを記述します。
グラフを描画したい時は、このコンポーネントを呼び出します。

`components/charts/chart-card/linechart.vue`

```vue
<script>
import { Line } from 'vue-chartjs';
export default {
    extends: Line,
    name: 'chart',
    props: ['chartData', 'options'],
    mounted() {
        this.renderChart(this.chartData, this.options)
    }
}
</script>
```

## カード作成 + グラフ描画

`components/charts/chart-card/chart-card.vue`

Vuetifyの、`v-card`でカード部分を実現。 
メインコンテンツを実装する`v-card-text`に、グラフ部分のコンポーネントを配置します。

### template部

```vue
<template>
    <v-card min-width="80%">
        <v-card-title>
            Simple Chart
        </v-card-title>
        <v-card-text class="chart-container"> 
            <Chart :chartData="chartData" :options="options"/>
        </v-card-text>
    </v-card>
</template>
```

### script部

data()で、グラフ描画に必要な情報を記述  

* chartData
    * labels: X軸のラベル (Array)
    * datasets
        * label: 各線の凡例
        * data: データの数値
        * fill: 線グラフより下を塗りつぶすか
        * backgroundColor: 塗りつぶす色
        * borderColor: 線の色


* options
    * responsive: レスポンシブのOn/Off
    * maintainAspectRatio: 縦横比を自動調整するか
    * legend: 凡例の設定

```vue
<script>
import Chart from './linechart.vue';
export default {
    components: {
        Chart
    },
    data() {
        return {
            chartData: {
                labels: ['A', 'B', 'C'],
                datasets: [{
                    label: "sample",
                    data: [100, 150, 300],
                    fill: true,
                    backgroundColor: 'rgba(83, 224, 156, 0.2)',
                    borderColor: '#53e09c'
                }]
            },

            options: {
                responsive: true,
                maintainAspectRatio: false,
                legend: {
                    display: true
                },
            }
        }
    },
}
</script>
```

## これをpageで描画

作成した、カードコンポーネントをpageで描画します。

```vue
<template>
    <div>
        <v-row>
            <v-col cols="12" sm="12" md="6" lg="4">
                <chartCard />
            </v-col>
        </v-row>
    </div>
</template>
<script>
import chartCard from '~/components/charts/chart-card/chart-card.vue';
export default {
    components: {
        chartCard,
    }
}
</script>
```

<img width="70%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152224/c5f2dae7-8f08-b03b-8c25-94875d3a3d03.png">

# Vue Storeからの取得

`components/charts/chart-card-withStore.vue`

ベタガキで使うタイミングなんてそんなにないので、   
よくNuxtで用いるパターンとして、Vuexを使うパターンで実装します。

**ベタ描き部分との差分のみ記述します**

## Store (Vuex)を作成

`store/chart.js`に、Chart用のStoreを作成。

**参考**
* [【Vue.js】Vuexの使い方を解説【データ保存】 | CodeCampus](https://blog.codecamp.jp/vuejp-vuex-commentary)

``` js
export const state = () => ({
    simple: {
        label: ['AA', 'AB', 'AC'],
        data: [1000, 2000, 2500]
    },
})
```

## カード作成 + グラフ描画

`this.$store.state...`で、NuxtのどこからでもStoreの値にアクセス可能なので、  
chartDataの、labelsとdataの情報を、Storeから引っ張ってくるように記述。

`+++` : 前の手順からの追記箇所

```vue
...

<script>
import Chart from './linechart.vue';
export default {
    (...)

    data() {
        return {
            (...)
            chartData: {
+++             labels: this.$store.state.charts_store.simple.label,
                datasets: [{
                    label: "sample",
+++                 data: this.$store.state.charts_store.simple.data,
                    fill: true,
                    backgroundColor: 'rgba(235, 70, 52, 0.2)',
                    borderColor: '#eb4634',
                }]
            },
            (...)
}
</script>
```

## pageに追記すると

`+++` : 前の手順からの追記箇所
`(...)`: 省略

```vue
<template>
    <div>
        <v-row>
            (...)
+++         <v-col cols="12" sm="12" md="6" lg="4">
+++              <storeChartCard />
+++         </v-col>
        </v-row>
    </div>
</template>
<script>
(...)
+++ import storeChartCard from '~/components/charts/chart-card/chart-card-withStore.vue';

export default {
    components: {
        chartCard,
+++     storeChartCard,        
    }
}
</script>
```

これで、Storeの情報を使って、グラフ描画できました。

<img width="70%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152224/8246872d-4b25-b5af-c8ab-dcf436fafd53.png" />

# 外部APIでの実行

`components/charts/chart-card-withAPI.vue`

Webアプリで一番使うのが、外部APIから情報を取得して、画面に描画する方法だと思います。なのでそのパターンを実装してみましょう。

今回は、日経平均株価のサンプルでやるために、[Quandl](https://www.quandl.com/)で取得してみます。

## RuntimeConfig | 外部APIのアクセストークン管理
APIを活用する時に、一番問題なのは、Nuxt.jsにToken情報などが晒されてしまう問題なので、今回は、Nuxt.jsの[RuntimeConfig](https://ja.nuxtjs.org/docs/2.x/directory-structure/nuxt-config#runtimeconfig)を使います、

**かなり重要**
本来は、`privateRuntimeConfig`を使うのですが、SSR化が必要だったりと色々設定が必要なので、今回は簡単するために、`publicRuntimeConfig`で実装します。ブラウザでAPIリクエストするので、**アクセストークンは見えてしまう実装**となります。完全に隠す必要がある場合は、SSR化して、privateRuntimeConfigで実装する必要があります。

### publicRuntimeConfigの設定

APIアクセストークンを管理するために以下の手順を取ります。

* 環境変数ファイル '.env'を作成
* .envファイルの中を `nuxt.config.js`から読み込み

#### 環境変数ファイル作成
Nuxt.jsにおいて、環境変数情報は、[.envファイル](https://qiita.com/fj_yohei/items/c77bff6f0177b4ff219e)で管理します。

```env
QUANDL_KEY=<Quandlのアクセストークン>
```

そして、`nuxt.config.js`に、 `process.env.変数名`で .envファイルの中見を参照できます。

```js
  publicRuntimeConfig: {
    quandlKey: process.env.QUANDL_KEY | <トークンベタガキ>
  },
```

#### 利用方法
コンポーネント等から、`this.$config.quandlKey`とすることで、環境変数の値を参照することができます。

## カード作成 + グラフ作成

### コード全体

`===` : 前の手順からの変更箇所
`+++` : 前の手順からの追記箇所
`(...)`: 省略

<details><summary>コード全体</summary>
<div>

```vue
<template>
    <v-card min-width="80%">
            (...)
===       <Chart v-if="loaded" :chartData="chartData" :options="options"/>
===       <v-progress-circular v-else-if="loaded!=true" indeterminate color="#53e09c" />
        </v-card-text>
    </v-card>
</template>

<script>
+++ import axios from 'axios';
import Chart from './linechart.vue';

export default {
    (...)
    data() {
        return {
+++         loaded: false,

            chartData: {
===             labels: null,
                datasets: [{
                    label: "average value",
===                 data: null,
                    fill: true,
                    backgroundColor: 'rgba(232, 229, 74, 0.2)',
                    borderColor: '#e8e54a',
                }]
            },
            (...)
        }
    },
+++    async created() {
+++        const url = "https://www.quandl.com/api/v3/datasets/CHRIS/CME_NK2/data.json?api_key=" + this.$config.quandlKey;
+++        const response = await axios.get(url);
+++        
+++        const data = response.data.dataset_data.data;
+++        const last_7day = data.slice(0, 7).reverse();
+++        
+++        // // 直近7日の日付 + 終値
+++        const days = last_7day.map(item => item[0]);
+++        const last = last_7day.map(item => item[4]);
+++
+++        await this.$set(this.chartData, 'labels', days);
+++        await this.$set(this.chartData.datasets[0], 'data', last);
+++
+++        this.loaded = true;
+++
+++    },
}
</script>
```

</div>
</details>

### APIからの情報を取得
まず、画面描画前にデータを取得する処理を実行するために、`created()`関数内でAPIを実行します。  
`created()`関数は、画面のレンダリング前に実行した処理を記載します。  
どのタイミングで、created()が呼ばれるかは、[Nuxt Lifecycle](https://ja.nuxtjs.org/docs/2.x/concepts/nuxt-lifecycle/)にわかりやすいイラストがあります。

created() 内では、以下の処理を行っています.

1. axios.getで日経平均株価データ取得
1. レスポンスデータから、直近7日の情報を取得(slice)、日付の早い順にソート
1. map関数で、日付・終値のArrayを抽出
2. storeに登録
3. 情報取得処理が完了したフラグを立てる

url内で、APIキーを付与するので、 `this.$config.quandlKey` でRuntimeConfigから、呼び出しています。

```js
    async created() {
        const url = "https://www.quandl.com/api/v3/datasets/CHRIS/CME_NK2/data.json?api_key=" + this.$config.quandlKey;
        const response = await axios.get(url);
        
        const data = response.data.dataset_data.data;
        const last_7day = data.slice(0, 7).reverse();
        
        // // 直近7日の日付 + 終値
        const days = last_7day.map(item => item[0]);
        const last = last_7day.map(item => item[4]);

        await this.$set(this.chartData, 'labels', days);
        await this.$set(this.chartData.datasets[0], 'data', last);

        this.loaded = true;

    },
```

### データ取得まで、レンダリングを待つ
Chart.jsは、templateのレンダリング次にデータが固定されるので、

* created()で、情報を取得仕切る前に、グラフ部分のレンダリングが行われる
* 結果、空白のグラフが生成される

という問題があります。なので、以下の対策を取ります。

1. データの取得が完了するかどうかを判断する変数、`loaded`を定義
2. templateの\<Chart>部分で、`v-if`を用いてレンダリング制御
3. created()の末尾で、loaded = trueに変更

`v-if`で、false状態にして、描画対象から外すことで、初回のレンダリング時に空で描画されるのを回避して、 `v-if`がtrue状態になった時に、Chartのレンダリングが実行されるようになります。

#### template
レンダリング制御のために、 \<Chart>に、`v-if`を追記します。
また、読み込み中にローディング表示したいので、Vuetifyの `v-progress-circular`を、`v-else-if`で表示するようにします。

```vue
<template>
    <v-card min-width="80%">
            ....
===       <Chart v-if="loaded" :chartData="chartData" :options="options"/>
===       <v-progress-circular v-else-if="loaded!=true" indeterminate color="#53e09c" />
        </v-card-text>
    </v-card>
</template>
```

### pageに追記すると

API実行中は、ローディング画面になり、
データの読み込みが終わったらグラフ描画が走るようになります。

`+++` : 前の手順からの追記箇所
`(...)`: 省略

```vue
<template>
    <div>
        <v-row>
            (...)
+++         <v-col cols="12" sm="12" md="6" lg="4">
+++              <outsideAPIChartCard />
+++         </v-col>
        </v-row>
    </div>
</template>
<script>
(...)
+++   import outsideAPIChartCard from '~/components/charts/chart-card/chart-card-withAPI.vue';

export default {
    components: {
        chartCard,
        storeChartCard,
+++     outsideAPIChartCard,        
    }
}
</script>
```
**output**
![loadapi.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152224/effaafb7-8244-c9ae-d22a-ed2d22cc23a8.png)

と実装できました。

# まとめ
今回は、Vuetify + Chart.jsで、ダッシュボードに使えそうなグラフのコンポーネントを作成してみました。
今後。GitHubにギャラリー的にまとめようと考えています。

# Source Code

https://github.com/sasayabaku/NuxtJS_Training/tree/main/data-visualization