スマートXXを連携させて、体重／体脂肪率／摂取カロリー／栄養素などがわかる自分専用ダッシュボードを作ってみた

この記事は[GWアドベントカレンダー](https://gw-advent.9wick.com/)の[「なにか新しいことにチャレンジするカレンダー」](https://gw-advent.9wick.com/calendars/2020/67)の5/3記事です。

---


自分は趣味でスポーツを幅広くやってます。しかし、外出がしにくい世の中の状態のため、最近はなかなかできていません。（バスケやるとか完全に密ですね）
家でできることは何かな〜と考えていたところ、体の状態をちゃんと記録して一覧で見えるダッシューボードを作ってみました。


まずは作ったものから。

<img width="1173" alt="スクリーンショット 2020-04-28 19.42.49.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227015/ea9b47ec-5f2b-5c9c-fbaa-0fe83080eb5e.png">


- 左側の上が体重／体脂肪率で、変動が大きいので7日間移動平均を濃い色で表示しています。
薄い色は生データ。

- 左下のはカロリー管理で、当日消費したカロリーを緑の線（体重と同様に７日間移動平均）で表示して、そのしたの棒グラフが栄養素別の摂取カロリーです。 (P:タンパク質、F:脂質、C:炭水化物)

- 右上に数字で体重／体脂肪率／筋肉量の1週間前との差分を出してます

- 右上の線グラフは筋肉量の推移です。見方は体重と同じ。

- 右下の円グラフと棒グラフは、直近１週間の栄養バランスと、カロリープラスマイナスの推移です。


- 右上にある期間をいじると、各種グラフの期間が変動します。


## なんでつくったのか

最近良く太ったね！っていわれる気がして（被害妄想）、ちゃんと痩せようと思ったんですが、なんとなくで痩せようとしてても全然痩せなかったので、この自粛期間をきっかけに、数字でわかるような管理をしてちゃんと痩せようと思ったから作りました。


## どういう構成になっているか

システム図作ってみました。

![system.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227015/d60c1881-c10a-38f1-d115-8d049229806b.png)


いま自分の手元にはガーミンの[vivosport](https://www.garmin.co.jp/products/intosports/vivosport-slate/)というスマート活動量計があります。
これは、消費カロリーや心拍数や睡眠時間など、生体データを色々と撮ってくれる活動量計です。


また、家には[TANITAのスマート体重計](https://www.tanita.co.jp/product/g/_TRD906BK/)があり、スマホ連携してクラウドに体重を記録してくれてます。

食事管理としては（完全手動ですが）[MyFitnessPal](https://www.myfitnesspal.com/ja/)のサービスが良いと聞いて、そちらを使っています。


この３つのサービス、どれも単独ではすごくいいサービスなんですが、消費カロリーと運動量を同時に見たい！とか、体重変化と食事量の関係性を見たい！となるととたんに難しかったんですね。

それぞれスマホに専用アプリがあり、ついでにクラウドにも専用サービスがあったので、データをスプレッドシートに纏めた上で、GoogleDataPortalでダッシュボード化をしました。


## つくりかた

### GoogleAppScriptの利用

データの取得先というか、プログラムの動かす場所としてはGASを選択しました。
これは、

- 自分でサーバー立てて管理するのがめんどくさい
- ときどき生データを見たくなる（DBだと生データ見るのがめんどくさいけど、GASならSpreadSheetに入れとけばいいからすぐ見れる）
- GoogleDataPortalが必要とするデータの形式に下ごしらえするのがやりやすい

という点で選びました。

ただ、欠点も合って、

- web上での開発がめんどくさい
- デバッグしにくい
- HTTPリクエストなどのとき、GAS専用ライブラリを使わないといけない

というところがありました。

#### GASでの開発環境

クラウド上の開発環境をあまり使いたくなかったので、それを解決するために、[GoogleAppScriptをローカルで開発・ローカルから実行してみた](https://qiita.com/wicket/items/d55bb719eeb424b82601) ということをやって、ちょっと面倒くささをへらすようにしました。

また、GAS専用ライブラリがあるとローカルでテストできないので、GASAdapterクラスを作って、ローカルだったらnode.jsのリクエスト、GAS上だったらGAS専用ライブラリを使う　という切り分けをしていました。

↓切り替えているところの一例

```typescript

public async fetch(
    url,
    method: FetchMethod = "GET",
    params: any = {},
    headers = {}
  ): Promise<string> {
    console.log("fetch", url, method, params);
    if (this.env === "gas") {
      Utilities.sleep(100);
      if (method === "GET") {
        return await this.gasGetFetch(url, params, headers);
      } else if (method === "POST") {
        return await this.gasPostFetch(url, params, headers);
      }
    } else if (this.env === "local") {
      await new Promise((resolve) => setTimeout(resolve, 1000));
      if (method === "GET") {
        return await this.nodeGetFetch(url, params, headers);
      } else if (method === "POST") {
        return await this.nodePostFetch(url, params, headers);
      }
    }
    throw new Error("unknown env " + this.env);
  }

```

なお、GASかローカルかの判定は、GAS専用ライブラリの`UrlFetchApp`があるかないか　で分けています。

```typescript

if (typeof UrlFetchApp !== "undefined") {
  this.setEnv("gas");
} else {
  this.setEnv("local");
}
```

ここのプログラムは公開しています
https://gist.github.com/9wick/5c10391f2ce65f05e197c7c7ffdf7297#file-gasadapter-ts

#### データベースとしての構築

どのアプリの情報も、日付と項目に対してデータが取れれば良いので、そこは共通化しました。
最終的にはこういう形のデータ入力をします。
![スクリーンショット 2020-05-03 19.16.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227015/8b7369d0-6555-4fc9-71f9-704015635331.png)

機能としては

- sheet名を選択して
- 特定の日の行を選んで（なければ作成して）
- 特定の項目の列を選んで
- そこの項目に数字を入力する

というだけです。

基本それほど難しくなく作れたかなと思いますが、
困ったポイントとしては、時間制限がありました。GASは５分ぐらい稼働し続けてるとタイムアウトで止まるんですが、スプレッドシートへの読み書きってすごい遅いんですね。1回の読み書きで500msぐらいかかるイメージしてます。

なので、1回の読み書きで大量にかけるように／読み込み回数へらすように、キャッシュを作りました。
プログラム内でスプレッドシート城のデータを再現し、そこに書き込み、実際のスプレッドシートへの書き込みはそのキャッシュを書き込むことで、スプレッドーシート↔GAS間のデータのやり取り頻度を減らしています。

キャッシュしてるところの一例

```typescript


  startCache() {
    this.useCache = true;

    const lastCol = this.sheet.getDataRange().getLastColumn();
    const lastRow = this.sheet.getDataRange().getLastRow();
    this.cachedSheet = this.sheet.getRange(1, 1, lastRow, lastCol).getValues();
  }

  writeCache() {
    if (this.useCache) {
      const lastCol = this.cachedSheet[0].length;
      const lastRow = this.cachedSheet.length;
      this.sheet.getRange(1, 1, lastRow, lastCol).setValues(this.cachedSheet);
    }
  }

 private setCellValue(row: number, col: number, value: any) {
    if (!this.useCache) {
      return this.sheet.getRange(row, col, 1, 1).setValue(value);
    }
    for (let i = this.cachedSheet[0].length; i < col; i++) {
      this.cachedSheet.forEach((e) => e.push(""));
    }
    const maxCol = this.cachedSheet[0].length;
    for (let i = this.cachedSheet.length; i < row; i++) {
      this.cachedSheet.push(new Array(maxCol).map((e) => ""));
    }
    this.cachedSheet[row - 1][col - 1] = value;
  }
```


こちらも全体像のプログラムを置いておきます
https://gist.github.com/9wick/5c10391f2ce65f05e197c7c7ffdf7297#file-spreadsheetadapter-ts

### 各種アプリからのデータ読み込み

それぞれのサービスがクラウドにデータを持っているので、そこからデータを引っ張ってきます。

| サービス名       |       クラウドURL |    API有無    |
|:-----------------|:------------------|:------------------:|
| GarminConnect   | https://connect.garmin.com/ |        ○        |
| HealthPlanet    | https://www.healthplanet.jp/ |       ✗       |
| MyFitnessPal    | https://www.myfitnesspal.com/ja/ |        △       |



HealthPlanetは[APIがある](https://www.healthplanet.jp/apis/api.html)ので、素直にそこから持ってきます

MyFitnessPalも[APIがある](https://www.myfitnesspal.com/ja/api)んですが、利用者登録がなぜかエラーになってできないので、諦めてスクレイピングしました。

GarminConnectはAPIがないので、これもスクレイピングしました。

スクレイピングの話は本流と外れるので割愛します。


データを取得したら、上で書いたSpreadsheetAdapterを使ってスプレッドシートに記録します。


### GoogleDataPortalでビジュアライズ化

やっとここまできました。データはスプレッドシートに記載されているので、あとはビジュアライズ化です。
GoogleDataPortalを選んだのは、単純に使ったことなかったので使ってみたかったからです。

[GoogleDataPortalのサイト](https://datastudio.google.com/u/0/navigation/reporting)から、新規で作成していきます。
![スクリーンショット 2020-05-03 19.31.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227015/a3cc03fc-f95b-c6d7-e724-5beb65d660e8.png)

データソースを何にしますかと聞かれるので、スプレッドシートを選んで、データがアップロードされるシートまで選択します

![スクリーンショット 2020-05-03 19.31.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227015/a7d8bb84-d0d0-731e-8561-d6af3b28bde9.png)


そしたら作成画面に移ります

![スクリーンショット 2020-05-03 19.32.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227015/260f5fc6-2ed8-e325-dab5-0ad0bd830d2a.png)


グラフを追加ボタンから、好きなグラフ（今回は期間の折れ線グラフ）を選ぶと
![スクリーンショット 2020-05-03 19.32.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227015/2800e85d-5f68-a15a-76a5-348dad680593.png)

とりあえずグラフができます。

![スクリーンショット 2020-05-03 19.35.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227015/f1b11520-9698-e93f-9f4c-50d42fb20626.png)

右側のディメンションや指標をいじると、それなりのグラフが簡単にできます。
また、データの欠損（体重はかってない日とか）があると見づらいので、グラフのスタイルで欠落データを線形補間に変えると便利です
![スクリーンショット 2020-05-03 19.33.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227015/24e4bbfe-2033-6ff4-d469-c427338797ae.png)

あとはコレを繰り返してグラウを作っていきます。

<img width="1173" alt="スクリーンショット 2020-04-28 19.42.49.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227015/c564e93f-27fb-b640-f773-5c8adbd25ebc.png">



#### GoogleDataPortalでできなかったこと

直接GoogleDataPortalでできなかったこととして、集計や差分がありました。
たとえば円グラフで１週間合計で摂取した栄養素の割合を出したかったのですが、それをGoogleDataPortalでは直接作ることができません。

なので、spreadsheetの別シートで集計して、それをGoogleDataPortalに反映させるというちょっと手間かかる形でやってます。


### GoogleDataPortalでよかったこと

サクッとビジュアライズできるのはよかったです。
また、公開範囲を自由に決めれる（スプレッドシートとかと同じ）なので、他人への共有も便利だと思いました。
(まぁこんな体重データを見せる人はいないですが笑)


## まとめ

ダイエットやバルクアップをするのに、ダッシュボード作るとはかどります！