Cloud Firestoreの時系列データをChart.jsでグラフ化するには？

## はじめに
[Firestore](https://firebase.google.com/docs/firestore?hl=ja)に貯まっていく時系列データをWeb上で視覚化させるために、
[Chart.js](https://www.chartjs.org/)を使用してグラフを作成させてみました。
本記事は、それを実現する上で自分にとってポイントとなった点をピックアップし、
備忘録としてまとめております。

FirestoreとChart.js、いずれも使ったことないよという方が、
*「あ、こんなこと出来るんだね」*と知見や興味を増やすことが出来るきっかけとなれば幸いです。

## 環境
* Ubuntu 18.04.3
* Node.js : v12.16
* vue@&#xFFF0;2.6.11
* firebase@&#xFFF0;7.13.1
* chart.js@&#xFFF0;2.9.3
* chartjs-plugin-streaming@&#xFFF0;1.8.0
* vue-chartjs@&#xFFF0;3.5.0
* moment@&#xFFF0;2.24.0

関連リンク  
* [Node.js](https://nodejs.org/ja/)
* [Firebase CLI](https://firebase.google.com/docs/cli?hl=ja)
* [Chart.js](https://www.chartjs.org/)
* [Moment.js](https://momentjs.com/)

## 結論：どんなグラフが出来たのか？
![image01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/499855/fb1402e8-2d8a-ed87-414b-989d0cdad1f9.png)

グラフのデータは説明用のサンプルです。
Firestoreに入っている温度と日付のデータをもとに、ブラウザ上でグラフが視覚化されている状態です。  

## Firestoreにデータを入れてみる
FirestoreはNoSQLですので、柔軟なデータ構造が可能です。
ここでは「試しに手動で入れるとどんな感じなのか？」という確認と、
「ソースコードからデータを入れる場合は？」という2つのやり方を見てみましょう。

### コンソールから手動で追加する場合
![image02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/499855/e7ec4970-7ad0-923b-ca67-f3fee9b9e6a2.png)
上の画像は、コンソールから手動でデータを入力している様子です。  
Firestoreでは、以下のような構成でデータが保管されます。
* `コレクション` - データを入れるフォルダのようなもの
* `ドキュメント` - フォルダに入った書類のようなもの
* `データ` - ドキュメントに載せる内容の数々

図では、事前にコレクションを作成しています。  
はじめに何かしらデータを入れることを求められ、上図のような画面が出てきます。  

今回は「室温の変化」を例に、温度と時間を入れるフィールドを用意しました。
具体的には次のとおりです。
* `Temp` : 温度。タイプは`number`で数値。
* `DateTime` : 日時。タイプは`timestamp`で日時。

タイプの詳細については、以下の公式のページを参照して下さい。
*[サポートされているデータ型](https://firebase.google.com/docs/firestore/manage-data/data-types?hl=ja)*

保存すると、以下のようにデータが蓄積されています。
![image03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/499855/6cc9381d-6124-2ebb-271a-3407b0eb9694.png)
### ソースコードからデータを追加する場合
今回、私はNuxt.jsの環境で作成しました。
公式ページでは様々な言語でのデータ追加方法について記載されていますので、
もし参考にされる場合はお使いの言語に置き換えていただければと思います。  

#### 準備：Firebaseプロジェクトでアプリを作成しておく
まずはアプリをFirebaseに追加しておく必要があります。
![image04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/499855/75a37111-b0da-387c-4583-d16e28295ab2.png)


アプリはプロジェクト概要の歯車アイコンから、「プロジェクトを設定」を選択します。
アプリを追加し、今回はWebを選択します。
![image05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/499855/f146f39e-ad0a-030a-9f98-bd918f880adb.png)

アプリの名前は、適当な名前をつけます。
ここでの名前はコンソール上で見られるだけの見出しにすぎないので、何でもOKです。

次に、スクリプトが以下のように表示されます。
![image06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/499855/d81df878-4aea-736a-152f-0d2cb8fa8a5a.png)

これらを使用するので、ソースコードの中に放り込んでおいて下さい。
ちなみに今すぐこの画面でメモしなくても、いつでも確認は可能です。

#### データ追加までの準備
まずは`index.vue`から。
ここにごちゃごちゃ書くのを避けておきます。

```vuejs:index.vue
<template>
  <chart/>
</template>

<script>
import chart from '~/components/chart.vue'

export default {
  components: {
    chart
  }
}
</script>
```

続いて`chart.vue`というグラフを描画するコードを用意。  
`<canvas>`内にグラフを描画します。  

```vuejs:chart.vue
<template>
  <div>
    <canvas id="myChart" width="800px" height="600px"></canvas>
  </div>
</template>
```
必要なライブラリをインポートし、先程のスクリプトをそのまま貼り付けます。

```vuejs:chart.vue
<script>
import { Line } from "vue-chartjs";
import "chartjs-plugin-streaming";

const firebase = require("firebase/app");
import "firebase/firestore";
import { firestore } from 'firebase';
import moment from "moment";

var firebaseConfig = {
  apiKey: "***************************************",
  authDomain: "********************.firebaseapp.com",
  databaseURL: "https://********************.firebaseio.com",
  projectId: "********************",
  storageBucket: "********************.appspot.com",
  messagingSenderId: "**************",
  appId: "*****************************************"
};
```
Firebaseアプリインスタンスを作成し、初期化します。
実は`Firebase App named '[DEFAULT]' already exists`というエラーにかなりはまってしまいました。
どうやら`firebase.apps.length`が初回は`0`なのですが、修正して更新かけると`1`になり、もうinitializeAppする必要がないようです。

```javascript:chart.vue

if (!firebase.apps.length) {
  firebase.initializeApp(firebaseConfig);
} else {
  firebase.app();
}
let db = firebase.firestore();

```

#### データを追加する  
いよいよデータをFirestoreに追加してみましょう。
ソースは先程の続きからです。
  
ここでは`setData()`というデータを追加する関数を作ってみましょう。

```javascript:chart.vue
export default {
  extends: Line,

  mounted() {
    let fireStoreDB = db.collection("【コレクション名】");
    function setData() {
      let temp = 20.2;
      let stringDate = '2020/04/01';
      let stringTime = '13:00:00';
      let date = stringDate + " " + stringTime;
      fireStoreDB.doc().set({
        Temp: temp,
        DateTime: firestore.Timestamp.fromDate(new Date(date))
      });
    }
```

上記のように、

```json
  {
    "フィールド名": "値",
    "フィールド名": "値"
  }
```

という形式でデータを追加できることが分かりますね。
ソース内ではまだ`setData()`を呼び出していませんが、
実行されると以下のようにFirestoreの指定したコレクション内に格納されます。  
  
![image07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/499855/3c2723c0-9185-7f93-2d28-3de06e928263.png)

#### データを読み込む
続いてはFirestoreのデータを読み込む関数を作ってみましょう。  

```javascript:chart.vue
    function getData() {
      let queryInfo = fireStoreDB.orderBy('DateTime');
      queryInfo.get().then(snapshot => {
        snapshot.docs.forEach(doc => {
          let item = doc.data();
          let time = item.DateTime;
          let temp = item.Temp;
          let datetime;
          if (time !== undefined && temp !== undefined) {
            datetime = new Date(time * 1000);
            dataArr = {
              time: datetime,
              temp: temp
            };
            addData(myChart, dataArr.time, dataArr.temp);
          }
        });
      });
    }
```

ポイントは、`orderBy('DateTime')`とつけることで、DateTimeでソートしているという点です。
これがないと、ドキュメントID順で読むため、以下のようなヘンテコな時系列のグラフになってしまうので注意です。  

![image08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/499855/62f67526-4cd6-cd41-6c5b-db590674d6be.png)

`addData()`関数で、Firestoreから取得したデータをグラフに反映させていきます。
この関数については後述します。
#### グラフの設定を決める

続いて`Chart.js`に関する部分です。

```javascript:chart.vue
    let ctx = document.getElementById("myChart").getContext("2d");
    let config = {
      type: "line", // 折れ線グラフ
      data: {
        datasets: [
          {
            label: "Temp", // ラベル名
            data: dataArr.temp, // グラフにしたいデータ
            backgroundColor: "rgba(50,50,255,0.1)", // グラフの背景色
            borderColor: "rgba(50,50,255,1)", // グラフの線の色
            fill: true, // グラフの背景色を塗りつぶし方（"top"にすると反転したり）
            lineTension: 0.4, // ベジェ曲線の度合い
                              // （0.5超えてくると不自然なラインに...）
          }
        ]
      },
      options: {
        title: {
          display: true, // グラフのタイトルを表示する
          text: "室温データ", // グラフのタイトル
          fontSize: 30 // タイトルのフォントサイズ
        },
        scales: {
          yAxes: [ // Y軸の設定
            {
              ticks: {
                min: 17, // 値の表示範囲（下限）
                max: 22 // 値の表示範囲（上限）
              }, // ticksは省略するとデータ内容に応じて自動で範囲を決めてくれます
              scaleLabel: {
                display: true, // 凡例の表示
                labelString: "Temp", // 凡例の名前
                fontSize: 15 // 凡例のフォントサイズ
              }
            }
          ]
        },
      }
    };
```

この辺のカスタマイズは[公式ドキュメント](https://www.chartjs.org/docs/latest/charts/line.html)を読んだほうが詳しく幸せになれますのでご覧ください。  

#### グラフにデータを追加する
最後は先述した`addData()`の中身です。

```javascript:chart.vue
    let myChart = new Chart(ctx, config);

    function addData(chart, time, temp) {
      console.log(time, temp);
      moment_time = moment(time, "x").format("MM/DD HH:mm:ss");
      chart.data.labels.push(moment_time);
      chart.data.datasets.forEach(dataset => {
        dataset.data.push({
          x: time,
          y: temp
        });
      });
      chart.update();
    }

    getData();
    setData(); // Firestoreにデータを追加する場合のみ呼び出しましょう
  }
};

</script>
```

ポイントは、X軸のラベル名を作成する所です。
[Moment.js](https://momentjs.com/)を使用すると、時間の表記方法を簡単に設定できます。
あくまでもグラフのラベルのために処理している点に注意して下さい。

## クエリの方法を変える

上記で紹介した方法ですと、`DateTime`の全データを見に行きますので、
データが多ければ多いほどFirestoreの読み取り量が増加します。
**Firestoreの無料枠は、**2020/4/1現在で**1日50,000まで**となっております。
  
ここでは自分が試したほんの数例を紹介するまでですが、さらに色々知りたい方は[公式ドキュメント](https://firebase.google.com/docs/firestore/query-data/queries?authuser=0)を読んでみましょう。

### 1時間以内のデータのみを取得したい

今から1時間以内のデータのみ取得したい場合、私は以下のようにしてみました。

```javascript

/* 今の日時を用意して */
let now = new Date();

/* Moment.jsを使って1時間前を計算して */
now = moment(now).subtract(1, 'h');
now = new Date(now);

/* .whereで条件を追加する */
let queryInfo = fireStoreDB.where('DateTime', '>', now).orderBy('DateTime');

```
  
`moment(指定した時間).subtract(引きたい時間, 単位)`というように、
Moment.jsにお世話になると時間計算も簡単ですね。

ちなみに引き算ではなく足し算の場合は、
`moment(指定した時間).add(足したい時間, 単位)`となります。
お約束の流れですが詳細は[公式ドキュメント](https://momentjs.com/guides/#/warnings/add-inverted-param/)をどうぞ。

### 指定した件数のみを取得したい

取得するデータの個数を制限するのは非常に簡単です。

```javascript
/* .limit()を追加して件数を制限 */
let queryInfo = fireStoreDB.orderBy('DateTime').limit(7);

```

ちなみにこれでグラフはどのようになったかというと、以下のとおりです。  

![image09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/499855/59b26488-a70f-f5be-da81-b807633c4a1a.png)


`.orderBy('DateTime')`で時系列順になったデータの頭から7件を拾っていることが分かりますね。
ただ、このままですと最新の7件にはなっておりません。

ではどうすれば良いか？

以下のように変更すれば可能になります。

```javascript
/* .limitToLast()を追加して件数を制限 */
let queryInfo = fireStoreDB.orderBy('DateTime').limitToLast(7);
```

![image10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/499855/95a15d10-f739-0e7c-03d7-cab16460c8cb.png)

これで無事最新の7件が時系列順に取得できました。

## おわりに
Firestoreは柔軟にデータが格納でき、それを本記事のように視覚化させてデプロイするだけで、
簡単にWeb上でそれらを共有できることが分かりました。
(いずれも触ったことなく形になるまで時間は要しましたが)

Chart.jsの部分だけでも、簡単に美しいグラフが描けることが伝わったのではないかと思います。
どうか皆さんのデータの視覚化に、少しでもお役に立てることを心より願っております。