DullCodes’s diary

programming,c++,python,MachineLearning,Math,Django,Competitive

初めてのDjango 18 図を書く

グラフを書くには

Djangoにはグラフを表示する機能はないので
そこら辺の描画はJavaScriptフレームワークがやる
そして、このグラフを描画するフレームワークも大量にある

D3.js - Data-Driven Documents

Beautiful HTML5 Charts & Graphs | 10x Fast | Simple API

Chart.js | Open source HTML5 Charts for your website

それぞれの特徴はよくわからないので
パット見で一番簡単そうだったchart.jsを使用する

Django

適当に設計

Project

体重と体脂肪率を記録するアプリ
2つのfloat の値を線グラフとして表現したいだけ

chart.js の使いかた

chart.js のページに飛んで Chart.min.js をインストールしてstaticに入れる
もしくはCDNでそのままインクルードするか、npm でインストール

model

こんなモデルを用意
ただグラフを使いたいだけなのでとにかく簡単に
アプリ名は healthy で
体重管理の英語表記を調べると weight manager とか weight controllerっていう
しっくりこない名前に
海外のアプリ名だとたいがいhealthyって付いているのでそれで

from django.db import models


class HealthyModel(models.Model):
    class Meta:
        db_table = 'healthymodel'
        verbose_name = "healthymodel"
        verbose_name_plural = "healthymodels"

    # fields
    weight = models.FloatField()
    fatrate = models.FloatField()
    # updated_at と全く同じデータだが、なぜかadmin管理画面からupdated_atがいじれなかった
    # のでむりやり別レコードでいじるようにする 完全に無駄 冗長
    input_date = models.DateTimeField()

    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

view/templates

html は一枚だけ

{% load static %}
<!DOCTYPE html>
<html lang="en">

<head>

  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Chart Sample</title>
</head>

<body>
  {% block main %}{% endblock main %}
  <script type="text/javascript" src="{% static 'js/Chart.min.js' %}"></script>
  {% block script %}{% endblock script %}
</body>

</html>

main の html

{% extends 'base.html' %}

{% comment %} main {% endcomment %}
{% block main %}
<main>
  <div>
    <h1>Kappa</h1>

    <!-- グラフを表示するのに html5 の機能である canvas を使う! -->
    <canvas id="chart"></canvas>
  </div>
</main>
{% endblock main %}

{% block script %}
<script>
// ここに色々書いていく
</script>
{% endblock script %}

Chart.js

適当な線グラフを作って表示

<script>
  // まずはchart のcanvas をもらう
  const ctx = document.querySelector('#chart').getContext('2d');

  // Chartjsに表示するdataを作る
  // data
  const data = {
    // datasets.data と1:1対応するラベル
    // 各データの下に表示される
    labels: ['1月17日', '2月17日', '3月17日'],

    // datasets: list
    // ここにデータを入れる
    datasets: [{
      label: '体重',
      data: [68.0, 70.0, 72.0],
      borderColor: 'rgba(255, 100, 100, 1)',
      fill: false, // 線より下の領域を塗りつぶすかどうか
    }]
  };

  // 細かなオプションが山のようにある 今回は無視
  const options = {};

  // Chartjs オブジェクトを作る
  // こいつに chart の canvas を渡すとcanvasに描画をしてくれる
  var myChart = new Chart(ctx, {
    type: 'line', // 線グラフを作るときには ここを'line', 棒グラフでは 'bar'
    data: data,
    options: options,
  });
</script>

f:id:DullCodes:20200327210444p:plain
django with chartjs one line graph

2つ以上の線を表示するには

datasets の中の dataをたくさん入れればいいだけ

<script>
  const ctx = document.querySelector('#chart').getContext('2d');
  // data
  const data = {
    labels: ['1月12日', '1月17日', '2月17日', '3月17日'],
    datasets: [{
      label: '体重',
      data: [70.0, 68.0, 70.0, 72.0],
      borderColor: 'rgba(255, 100, 100, 1)',
      fill: false,
    }, {
      label: '体脂肪',
      data: [19.0, 18.0, 19.0, 20.0],
      borderColor: 'rgba(100, 100, 255, 1)',
      fill: false,
    }]
  };
  // options
  const options = {};
  // Chart
  var myChart = new Chart(ctx, {
    type: 'line',
    data: data,
    options: options,
  });
</script>

f:id:DullCodes:20200327210938p:plain
django with chartjs two lines graph

すげぇ
というわけでモデルからレコード持ってきてdataに入れれば終了

実装

views.py

いろいろ調べたけどこれが限界だった

from django.shortcuts import render
from django.views import View
from .models import HealthyModel


class IndexView(View):
    def get(self, request, *args, **kwargs):
        weights = []
        fat_rates = []
        dates = []
        for record in HealthyModel.objects.order_by('input_date').all():
            weights.append(record.weight)
            fat_rates.append(record.fatrate)
            dates.append(record.input_date.strftime('%Y年%m月%d日'))

        ctx = {
            'weights': weights,
            'fat_rates': fat_rates,
            'dates': dates,
        }
        print(ctx)
        return render(request, 'healthy/index.html', context=ctx)

もっとどうにかならなかったのか
リストを3つ宣言するところとか最高にダサい

index.html

渡し先のtemplates ではいい感じ

{% extends 'base.html' %}

{% block main %}
<main>
  <div>
    <h1>Kappa</h1>
    <canvas id="chart"></canvas>
  </div>
</main>
{% endblock main %}

{% block script %}
<script>
  const ctx = document.querySelector('#chart').getContext('2d');
  // data
  const data = {
    labels: {{ dates|safe }}, // これで渡せる
    datasets: [{
      label: '体重',
      data: {{ weights|safe }}, // ここと
      borderColor: 'rgba(255, 100, 100, 1)',
      fill: false,
    }, {
      label: '体脂肪',
      data: {{ fat_rates|safe }}, // ここも
      borderColor: 'rgba(100, 100, 255, 1)',
      fill: false,
    }]
  };
  // options
  const options = {};
  // Chart
  var myChart = new Chart(ctx, {
    type: 'line',
    data: data,
    options: options,
  });
</script>
{% endblock script %}

{{ dates|safe }} というのは Django の templatesの機能で、htmlで使われるエスケープキャラクターを
うまいこと処理してXSSとかの対策をしてくれているとそうじゃないとか
上の例だと文字列を表示する時によしなにやってくれている
もしセーフをつけないと

f:id:DullCodes:20200328081351p:plain
some data from django to js

他にDjango -> JavaScriptでデータを渡す方法は
django側で json.dumps()して渡すと、ちゃんとjsonで読み込んでくれるらしい
JSON文字列にして渡すということ)
こっちのほうがきれいにかけそう? Chartで使うデータはそのまんまJavaScriptのオブジェクトなので
データだけを渡すのではなくて、Djanog側で全部データ作っちゃうのもいいかもしれない

# イメージ
raw_data = {
    data : {
        labels : ... ,
        datasets : [ ... ]
    }m ...
}
chart_json = json.dumps(raw_data)

return render (request, 'chart/draw.html', chart_json)

みたいな感じ

// 動かしていない
{% block script%}
<script>
    const ctx = chart.canves;
    const chart_data = {{ chart_json }}
    const myChart = new Chart(ctx, {
        type: chart_data['type'],
        data: chart_data['data'],
        options: chart_data['options'],
    });
</script>
{% endblock script%}

ちゃんと書くならこっちのほうが見やすそう
いや、JavaScript側でデータの整形をしたほうが見やすい気も?
どっちがいいんだろうかわからない またいつか

まとめ

というわけで最終出力

f:id:DullCodes:20200327233514p:plain
finished

とりあえず Django -> JavaScript のデータの受け渡しはとっても簡単にできるということがわかりました
Djangoでデータ作って JSでは {{ ctx }} で表示終了

あらためてグラフを見ると全体的にちょっとおかしい
canvasをきっちり左端から右端まで使っているグラフってそんなものない
普通グラフって左端、右端は空白があるはず そうじゃないと見づらい

こういう幅の設定や、x軸の始点・終点の設定などはoptionsに色々指定して行う けどもう眠いのでこれで終了

次は全く使えない英語力を鍛えるを鍛えるべく、いい感じの単語リストがエクセルでダウンロードできたので
エクセル -> DB -> とりあえずランダムに英単語 <-> 和訳みたいなフラッシュカード?形式のアプリを作る