【 Docker＋Nginx＋Django＋RDS】WEBアプリができるまで⑩成長曲線グラフを描いてみよう

# 前置き
独学で、子供の成長アプリを作った時のことを、記録として残していきます。
間違っているところなどあれば、ご連絡お願いします。
　①[Djangoのようこそページへたどり着くまで](https://qiita.com/quantity82/items/e8c2392bf982efb55f80)
　②[NginxでDjangoのようこそページへたどり着くまで](https://qiita.com/quantity82/items/88a6348135e25c661feb)
　③[カスタムユーザーを作ってadminにたどり着く](https://qiita.com/quantity82/items/b3cd6ded5bbb5f3726f3)
　④[ログインログアウトをしよう](https://qiita.com/quantity82/items/8e4b7ce2ef56e60ad392)
　⑤[ユーザー登録（サインイン）機能を作ろう](https://qiita.com/quantity82/items/bbfc7e72d4a65c71e24c)
　⑥[ユーザーごとのデータ登録できるようにする〜CRU編](https://qiita.com/quantity82/items/4d019a4ee475208a5a26)
　⑦[ユーザーごとのデータ登録できるようにする〜削除編](https://qiita.com/quantity82/items/c48fee71b5f8eb766dae)
　⑧[画像ファイルのアップロード](https://qiita.com/quantity82/items/9bcbd25902f6878ee325)
　⑨[身長体重を記録する＠一括削除機能つき](https://qiita.com/quantity82/items/c8e58742eeaa8c0fc954)
　⑩[成長曲線グラフを描いてみよう](https://qiita.com/quantity82/items/619f5ed2a6cd45f7f592)　<--ここです
　⑪[本番環境へデプロイ＋色々手直し](https://qiita.com/quantity82/items/6e91b7ee7a2d7e0fab32)


# Goal
登録された身長体重データをもとに、成長曲線を書いてみよう



# 成長曲線とは
横軸に年齢、縦軸に身長や体重をプロットする曲線。
厚生労働省が10年ごとに計測データを出していて、その標準偏差などと比較しながら
子供の成長具合を把握するのに使うもの。

> 平成２２年乳幼児身体発育調査の概況について
https://www.mhlw.go.jp/stf/houdou/0000042861.html



# View
グラフ描画はmatplotlib、pandas、numpyを利用。
４種類のグラフ描画を連結させて、１つのグラフとして描画している。

・固定線（身長、体重）
厚労省のデータはCSVにしてSTATICに保存。
データは折れ線グラフで描画。線自体は身長、体重ごとに７本ずつ描かれる。（標準偏差）


・分散図（身長、体重）
こちらは、[１つ前](https://qiita.com/quantity82/items/c8e58742eeaa8c0fc954)で登録できるようにした身長体重データを読み込んでプロットする。
身長体重データは計測日しか持っていないので、計測日時点の子供の年齢は、生年月日から計算する。
簡易な計算方法はググれば出てくるのだが、なぜか0才のときの月齢計算が成立しないので、
そのときだけ別の計算式になるよう工夫（？）している。


```phys/views.py
from django.shortcuts import render, redirect
from django.http import HttpResponse
from django.contrib.auth.decorators import login_required
from .models import PhysData
from users.models import KidsProfile
from .forms import PhysDataForm, PhysDataEditForm
import matplotlib.pyplot as plt
from matplotlib.backends.backend_agg import FigureCanvasAgg
import numpy as np
import pandas as pd
import io


# 身長体重データを昇順で返却
@login_required
def phys_data_list(request, **kwargs):
（略）

# 身長体重データを新規登録する
@login_required
def phys_data_add(request):
（略）

# 身長体重データを編集する
@login_required
def phys_data_edit(request, dataPostId):
（略）

# 身長体重データを削除する
@login_required
（略）

#グラフ画像を表示するページ
@login_required
def graph_page_display(request, **kwargs):
    user_name = request.user
    if len(kwargs) > 0:
        kidsProfileId = kwargs["kidsProfileId"]
    else:
        kidsProfileId = KidsProfile.objects.filter(user=user_name).order_by('id')[0].id

    kids_profiles = KidsProfile.objects.filter(user=user_name)
    kids_profile = KidsProfile.objects.filter(id=kidsProfileId)

    params = {
        'kidsProfiles' : kids_profiles,
        'kidsProfileId' : kidsProfileId,
        'kidsName' : kids_profile[0].name,
    }
    return render(request, 'phys/phys_graph_display.html', params)

# グラフの画像生成
@login_required
def graph_image_display(request, **kwargs):
    # 初期設定
    fig = plt.figure(figsize=(8, 5))
    ax_h = fig.add_subplot(1, 1, 1)
    ax_w = ax_h.twinx()
    ax_hs = ax_h.twinx()
    ax_ws = ax_h.twinx()

    # グラフ自体の設定
    # タイトル
    ax_h.set_xlabel(u'age')
    ax_h.set_ylabel(u'cm')
    ax_w.set_ylabel(u'kg')

    # 固定線の描画
    border_color_h = 'skyblue' #身長の固定線色
    border_color_w = 'lightgreen' #体重の固定線色
    border_lw_mid = 2 #中央の固定線太さ
    border_lw_other = 1 #中央以外の固定線太さ

    # 固定値（身長）
    # 3%
    data_man_height_03 = pd.read_csv('static/physData/man_height_03.csv', names=['num1', 'num2'])
    ax_h.plot(data_man_height_03['num1'], data_man_height_03['num2'], color=border_color_h, lw=border_lw_other)
    # 10%
    data_man_height_10 = pd.read_csv('static/physData/man_height_10.csv', names=['num1', 'num2'])
    ax_h.plot(data_man_height_10['num1'], data_man_height_10['num2'], color=border_color_h, lw=border_lw_other)
    # 25%
    data_man_height_25 = pd.read_csv('static/physData/man_height_25.csv', names=['num1', 'num2'])
    ax_h.plot(data_man_height_25['num1'], data_man_height_25['num2'], color=border_color_h, lw=border_lw_other)
    # 50%
    data_man_height_50 = pd.read_csv('static/physData/man_height_50.csv', names=['num1', 'num2'])
    ax_h.plot(data_man_height_50['num1'], data_man_height_50['num2'], color=border_color_h, lw=border_lw_mid)
    # 75%
    data_man_height_75 = pd.read_csv('static/physData/man_height_75.csv', names=['num1', 'num2'])
    ax_h.plot(data_man_height_75['num1'], data_man_height_75['num2'], color=border_color_h, lw=border_lw_other)
    # 90%
    data_man_height_90 = pd.read_csv('static/physData/man_height_90.csv', names=['num1', 'num2'])
    ax_h.plot(data_man_height_90['num1'], data_man_height_90['num2'], color=border_color_h, lw=border_lw_other)
    # 97%
    data_man_height_97 = pd.read_csv('static/physData/man_height_97.csv', names=['num1', 'num2'])
    ax_h.plot(data_man_height_97['num1'], data_man_height_97['num2'], color=border_color_h, lw=border_lw_other)

    # 固定値（体重）
    # 3%
    data_man_weight_03 = pd.read_csv('static/physData/man_weight_03.csv', names=['num1', 'num2'])
    ax_w.plot(data_man_weight_03['num1'], data_man_weight_03['num2'], color=border_color_w, lw=1)
    # 10%
    data_man_weight_10 = pd.read_csv('static/physData/man_weight_10.csv', names=['num1', 'num2'])
    ax_w.plot(data_man_weight_10['num1'], data_man_weight_10['num2'], color=border_color_w, lw=1)
    # 25%
    data_man_weight_25 = pd.read_csv('static/physData/man_weight_25.csv', names=['num1', 'num2'])
    ax_w.plot(data_man_weight_25['num1'], data_man_weight_25['num2'], color=border_color_w, lw=1)
    # 50%
    data_man_weight_50 = pd.read_csv('static/physData/man_weight_50.csv', names=['num1', 'num2'])
    ax_w.plot(data_man_weight_50['num1'], data_man_weight_50['num2'], color=border_color_w, lw=1.5)
    # 75%
    data_man_weight_75 = pd.read_csv('static/physData/man_weight_75.csv', names=['num1', 'num2'])
    ax_w.plot(data_man_weight_75['num1'], data_man_weight_75['num2'], color=border_color_w, lw=1)
    # 90%
    data_man_weight_90 = pd.read_csv('static/physData/man_weight_90.csv', names=['num1', 'num2'])
    ax_w.plot(data_man_weight_90['num1'], data_man_weight_90['num2'], color=border_color_w, lw=1)
    # 97%
    data_man_weight_97 = pd.read_csv('static/physData/man_weight_97.csv', names=['num1', 'num2'])
    ax_w.plot(data_man_weight_97['num1'], data_man_weight_97['num2'], color=border_color_w, lw=1)

    # 子供データ取得
    user_name = request.user

    if len(kwargs) > 0:
        kidsProfileId = kwargs["kidsProfileId"]
    else:
        kidsProfileId = KidsProfile.objects.filter(user=user_name).order_by('id')[0].id

    kids_profile = KidsProfile.objects.filter(user=user_name, id=kidsProfileId)
    phys_data = PhysData.objects.filter(user=user_name, kidsProfile=kidsProfileId)

    # グラフデータ作成
    graph_height = []
    graph_weight = []
    graph_date = []

    for i in range(len(phys_data)):
        graph_height.append(phys_data[i].height)
        graph_weight.append(phys_data[i].weight)

        # 誕生日計算（小数点計算）
        dStr1 = phys_data[i].date.strftime('%Y%m%d')
        dStr2 = kids_profile[0].birthday.strftime('%Y%m%d')

        if kids_profile[0].birthday.year == phys_data[i].date.year:
            dStr3 = (int(dStr1) - int(dStr2)) / 1000 # 0才のときの月齢を出す
        else:
            dStr3 = (int(dStr1) - int(dStr2)) / 10000 # 一般的な誕生日計算
        graph_date.append(dStr3)

    # グラフプロット（分散図）
    ax_hs.scatter(graph_date, graph_height, marker='x', color='blue')
    ax_ws.scatter(graph_date, graph_weight, marker='x', color='green')

    # グリッド設定
    ax_h.grid(which='both', color='gray', linestyle='solid')
    ax_w.grid(which='both', color='gray', linestyle='solid')

    # Y軸最大値設定
    ax_h.set_ylim([0, 140])
    ax_w.set_ylim([0, 35])
    ax_hs.set_ylim([0, 140])
    ax_ws.set_ylim([0, 35])

    # Y軸メモリ
    ax_h.set_yticks(np.linspace(0, 140, 15))
    ax_w.set_yticks(np.linspace(0, 35, 15))
    ax_hs.set_yticks([])
    ax_ws.set_yticks([])

    # X軸メモリ
    ax_h.set_xticks(np.linspace(0, 7, 8))
    ax_w.set_xticks(np.linspace(0, 7, 8))
    ax_hs.set_xticks(np.linspace(0, 7, 8))
    ax_ws.set_xticks(np.linspace(0, 7, 8))

    # 描画
    canvas = FigureCanvasAgg(fig)
    buf = io.BytesIO()
    canvas.print_png(buf)

    response = HttpResponse(buf.getvalue(), content_type='image/png')

    fig.clear()

    response['Content-Length'] = str(len(response.content))

    return response
```

CSVはこんな感じのデータ。左が年齢、右が身長。
原データはエクセルファイルなので、Excel上で加工してCSV化したもの。

```static/physData/man_height_03.csv
0.00,44.0
0.08,48.7
0.17,50.9
```


# Template ＆ URL
graph_image_displayはイメージファイルを返却してくるので、
urlsでURL化した上で、HTMLではそのURLを<img>タグで待ち受ける。
http://〜〜〜.pngみたいな形式でなくても画像が表示されるのは、何だか驚き。

画像描画たびに裏でグラフ描画が走るため、画像表示までに数秒かかってしまう。
そのため、画像が表示されるまではbootstrapのspinnerが表示されるようjsで制御。

```phys/phys_graph_display.html
{% extends 'base.html' %}

{% block extra_js %}
<script>
$(function() {
  $('#loader-bg ,#loader').css('display','block');//ローディング画像を表示
});

$(window).on("load",function () { //読み込み完了したら実行する
  $('#loader-bg').css('display','none');//ローディングを隠す
  $('#loader').css('display','none');
  $('#contents').fadeIn().removeClass("is-hide");//コンテンツを表示する
});

$(function(){
  setTimeout('stopload()',10000);　//いつまでもローディング状態にならないように10秒で強制表示させる

});

function stopload(){ //強制表示の関数
  $('#contens').css('display','block');
  $('#loader-bg').delay(900).fadeOut(800);
  $('#loader').delay(600).fadeOut(300);
}
</script>
{% endblock extra_js %}

{% block content %}
<div class="row">
    <div class="col-md-12 col-lg-2">
        <div class="list-group">
          {% for kidsProfile in kidsProfiles %}
            <a href="{% url 'phys:graph_page_display' %}{{kidsProfile.id}}" class="list-group-item list-group-item-action">{{kidsProfile.name}}</a>
          {% endfor %}
        </div>
        <br>
   </div>
    <div class="col-md-12 col-lg-7 overflow-auto">
        <div class="card">
            <div class="card-header">{{ kidsName }}</div>

            <div class="card-body">
                <div id="loader-bg">
                    <div id="loader">
                        <div class="text-center">
                            <div class="spinner-border" role="status">
                                <span class="sr-only">Loading...</span>
                            </div>
                        </div>
                    </div>
                </div>
                <img src="{% url 'phys:graph_image_display' kidsProfileId %}" id="contents" class="img-fluid is-hide">
            </div>
        </div>
    </div>
{% endblock %}
```
```phys/urls.py
from django.urls import path
from . import views

app_name = 'phys'

urlpatterns = [
    path('phys/list/', views.phys_data_list, name='phys_data_list'),
    path('phys/list/<kidsProfileId>', views.phys_data_list, name='phys_data_list'),
    path('phys/data_add/', views.phys_data_add, name='phys_data_add'),
    path('phys/data_edit/<dataPostId>', views.phys_data_edit, name='phys_data_edit'),
    path('phys/data_delete/', views.phys_data_delete, name='phys_data_delete'),
    path('phys/graph/', views.graph_page_display, name='graph_page_display'),
    path('phys/graph/<kidsProfileId>', views.graph_page_display, name='graph_page_display'),
    path('data/graph_imaga/', views.graph_image_display, name='graph_image_display'),
    path('data/graph_imaga/<kidsProfileId>', views.graph_image_display, name='graph_image_display'),
]

]
```


# 動かす！
画像が表示されるまでは、くるくる
<img width="1440" alt="スクリーンショット 2020-02-14 13.11.03.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547363/dc83b096-249e-fd1c-fcbc-23c23b19d7bb.png">
こんな感じで描画されます。
<img width="1440" alt="スクリーンショット 2020-02-14 13.11.08.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547363/50bc4dbf-c83e-61bf-d308-1d189c4be711.png">