2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-02-14

前置き

独学で、子供の成長アプリを作った時のことを、記録として残していきます。
間違っているところなどあれば、ご連絡お願いします。
 ①Djangoのようこそページへたどり着くまで
 ②NginxでDjangoのようこそページへたどり着くまで
 ③カスタムユーザーを作ってadminにたどり着く
 ④ログインログアウトをしよう
 ⑤ユーザー登録(サインイン)機能を作ろう
 ⑥ユーザーごとのデータ登録できるようにする〜CRU編
 ⑦ユーザーごとのデータ登録できるようにする〜削除編
 ⑧画像ファイルのアップロード
 ⑨身長体重を記録する@一括削除機能つき
 ⑩成長曲線グラフを描いてみよう <--ここです
 ⑪本番環境へデプロイ+色々手直し

Goal

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

成長曲線とは

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

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

View

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

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

・分散図(身長、体重)
こちらは、1つ前で登録できるようにした身長体重データを読み込んでプロットする。
身長体重データは計測日しか持っていないので、計測日時点の子供の年齢は、生年月日から計算する。
簡易な計算方法はググれば出てくるのだが、なぜか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をタグで待ち受ける。
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'),
]

]

動かす!

画像が表示されるまでは、くるくる
スクリーンショット 2020-02-14 13.11.03.png
こんな感じで描画されます。
スクリーンショット 2020-02-14 13.11.08.png

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?