Python(Django)でmodelデータの値を使用してchart.jsで棒グラフを表示する方法

Djangoで自作Webアプリを作る際に、Charts.jsへのデータの渡し方で悩んだので、備忘録です。
以下のような棒グラフを作成します。

更新

chart.jsのグラフってとっても綺麗だよね!
動きを見るには、↑の更新ボタンを押してみてね。

線がびよーんって伸びてくるのがかっこいいよな。

とりあえず、データベースは作ったんだけど、数値をどうやったらtemplateに反映できるの?

Charts.jsをDjangoで使用する方法です。
ポートフォリオ用に、学習時間管理のアプリを作成しました。
勉強した内容と時間を過去7日間分表示する棒グラフをcharts.jsで表示したかったのですが、modelのデータをtemplateに渡す方法でかなり悩みましたので、方法を残しておきます。

ソース

まずはソースから晒します。初めて自分で1から書いたコードなので、ここ変だよーというのがあっても見逃してください。

models.py

from django.db import models
from django.utils import timezone


class leraning_time(models.Model):
    id = models.AutoField
    day = models.DateField(default= timezone.now)
    time = models.IntegerField(blank= False, null=True)
    category = models.CharField(default="", max_length=200)
    details = models.CharField(default="", max_length=200, blank=True, null=True)
    startdt = models.DateTimeField(default=timezone.now)
    enddt = models.DateTimeField(default=timezone.now)

views.py

from django.shortcuts import render
from .forms import inputform
from .models import leraning_time
from django.db.models import Sum

#棒グラフ用データ

def index(request):
    category=[]
    time=[]
    day=[]

    queryset = leraning_time.objects.all().order_by('-id')[:7]
    for Leraning_time in queryset:
        category.append(Leraning_time.category)
        day.append(str(Leraning_time.day))
        time.append(Leraning_time.time)

    T1 = [0, 0, 0, 0, 0, 0]
    T2 = [0, 0, 0, 0, 0, 0]
    T3 = [0, 0, 0, 0, 0, 0]
    T4 = [0, 0, 0, 0, 0, 0]
    T5 = [0, 0, 0, 0, 0, 0]
    T6 = [0, 0, 0, 0, 0, 0]
    T7 = [0, 0, 0, 0, 0, 0]

    if len(time) > 0:
        T1.insert(0, time[0])
        T2.insert(1, time[1])
        T3.insert(2, time[2])
        T4.insert(3, time[3])
        T5.insert(4, time[4])
        T6.insert(5, time[5])
        T7.insert(6, time[6])

    return render(request, 'new_learn_mgr/index.html', {
        'C1': category[0],
        'C2': category[1],
        'C3': category[2],
        'C4': category[3],
        'C5': category[4],
        'C6': category[5],
        'C7': category[6],
        'T1': T1,
        'T2': T2,
        'T3': T3,
        'T4': T4,
        'T5': T5,
        'T6': T6,
        'T7': T7,
        'day': day,
    })

templateファイル (index.htmlという名前を付けました)

<!DOCTYPE html>
{% extends 'new_learn_mgr/base.html' %}
{% load static %}
{% block content %}
<html lang="jp">
<head>
    <meta charset="UTF-8">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous">
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/js/bootstrap.bundle.min.js" integrity="sha384-ygbV9kiqUc6oa4msXn9868pTtWMgiQaeYH7/t7LECLbyPA2x65Kgf80OJFdroafW" crossorigin="anonymous"></script>
    <title>Title</title>
</head>

<body>

途中関係ないのが多いので省略

<canvas id="myBarChart"></canvas>

<script src="https://cdn.jsdelivr.net/npm/chart.js@2.8.0"></script>
<script>
    var ctx = document.getElementById("myBarChart").getContext('2d');
    var myBarChart = new Chart(ctx, {
        type: 'bar',
        data: {
            labels: {{ day|safe }},
            datasets: [
            {
                label: '{{ C1|safe }}',
                data: {{ T1|safe }},
                backgroundColor:"rgba(255, 99, 132, 0.2)",
                borderColor:"rgba(255, 99, 132, 1)"
            },
            {
                label: '{{ C2|safe }}',
                data: {{ T2|safe }},
                backgroundColor:"rgba(255, 159, 64, 0.2)",
                borderColor:"rgba(255, 159, 64, 1)"
            },
            {
                label: '{{ C3|safe }}',
                data:  {{ T3|safe }},
                backgroundColor:"rgba(255, 205, 86, 0.2)",
                borderColor:"rgba(255, 205, 86, 1)"
            },
            {
                label: '{{ C4|safe }}',
                data:  {{ T4|safe }},
                backgroundColor:"rgba(75, 192, 192, 0.2)",
                borderColor:"rgba(75, 192, 192, 1)"
            },
            {
                label: '{{ C5|safe }}',
                data:  {{ T5|safe }},
                backgroundColor:"rgba(54, 162, 235, 0.2)",
                borderColor:"rgba(54, 162, ,235, 1)"
            },
            {
                label: '{{ C6|safe }}',
                data:  {{ T6|safe }},
                backgroundColor:"rgba(153, 102, 255, 0.2)",
                borderColor:"rgba(153, 102, 255, 1)",
            },
            {
                label: '{{ C7|safe }}',
                data:  {{ T7|safe }},
                backgroundColor:"rgba(132, 203, 207, 0.2)",
                borderColor:"rgba(132, 203, 207, 1)"
            }
            ]
        },
        options: {
            title: {
                display: true,
                text: '学習実施状況'
            },
            scales: {
                xAxes: [{
                stacked: true
                }],
                yAxes: [{
                    ticks: {
                        suggestedMax: 8,
                        suggestedMin: 0,
                        stepSize: 1,
                        callback: function(value, index, values){
                            return  value +  '時間'
                        }
                    }
                }]
            },
            legend:{
                labels:{
                    boxWidth: 20,
                }
            },
        }
    });
</script>

解説

STEP1:データテーブルを確認する

models.pyで定義したテーブルは以下のような感じでデータが入っています。
この表から直近7日分のデータを抜き出して、冒頭のようなグラフを作成します。
なので、一度この値を変数に格納して、templateに渡す必要がありますね。

STEP2:views.pyでmodelデータを取り出す

 その前に・・・凡例を複数作成する方法について

ここで大きなポイントは、グラフの凡例が上のデータテーブルで言うところの「内容」になります。
複数のラベルを作るには、一手間あります。
通常の棒グラフの書き方ですと、dataset部分は以下のようになり、グラフの凡例は1つしか出ません。

Sample1

data: {
      //データ項目のラベル
      labels: ["1日", "2日", "3日", "4日", "5日", "6日"],
      //データセット
      datasets: [{
          //凡例
          label: "Python",
          //グラフのデータ
          data: [3, 4, 3, 5, 2, 3]
      }]
  },

これですと、Python意外にも勉強しているのに、Python以外に何をしたのか分からなくなってしまいます。そこで、datasetsを複数データに分けて、各dataを7つの要素を持つリストにします。(7日分のデータを取るため)

例えば、1つ目のデータは直近1日目のデータになるので、リスト内の0個目の要素にその日の学習時間が入ります。
同じように2つ目のデータは直近2日目のデータなのでリスト内の1個目の要素にその日の学習時間が入ります。それ以降も同じようにします。

Sample2

data: { 
    labels:['7月7日','7月6日','7月5日','7月4日','7月3日','7月1日','6月29日'], //データ項目のラベル
      datasets: [
        {
          label: 'python', //1日目
          data: [2,0,0,0,0,0,0],             //ここがポイント!!
          backgroundColor: "rgba(200,112,126,0.5)"
        },{
          label: 'HTML', //2日目
          data: [0,1,0,0,0,0,0],             //ここがポイント!!
          backgroundColor: "rgba(80,126,164,0.5)"
        },{
          label: 'CSS', //3日目
          data: [0,0,5,0,0,0,0],             //ここがポイント!!
          backgroundColor: "rgba(230,180,34,0.5)"
        },{
          label: 'python', //4日目
          data: [0,0,0,2,0,0,0],             //ここがポイント!!
          backgroundColor: "rgba(200,112,126,0.5)"
        },{
          label: 'JavaScript', //5日目
          data: [0,0,0,0,7,0,0],             //ここがポイント!!
          backgroundColor: "rgba(80,126,164,0.5)"
        },{
          label: 'Django', //6日目
          data: [0,0,0,0,0,4,0],             //ここがポイント!!
          backgroundColor: "rgba(200,112,126,0.5)"
        },{
          label: 'PHP', //7日目
          data: [0,0,0,0,0,0,2],             //ここがポイント!!
          backgroundColor: "rgba(132, 203, 207, 0.2)"
        }
      ]
    },

こんな感じです。Sample1と2を見比べて2はちょっとごちゃごちゃしてしまいますが、凡例が入る器を7つ作ったと思ってください。
でもこのままでは以下のよう各日付に7つ分のデータがあることになり、グラフのレイアウトがおかしくなってしまいます。

7月7日のデータは[2,0,0,0,0,0,0]ですから棒グラフが左寄りに細く、
6月29日のデータは[0,0,0,0,0,02]なので、棒グラフが右寄りに細く表示されていますね。
値は0なのにちゃんとその分のスペースは空けてくれている優しい世界がありました。
でも見た目がちょっとアレなので棒の幅を広げましょう。

以下のようにoptionsで xAxes: [{stacked: true}]を設定します。
スタックなので、積み上げ棒グラフにしています。

Sample3

options: {
            title: {
                display: true,
                text: '学習実施状況'
            },
            scales: {
                xAxes: [{
                stacked: true    →ここがポイント!
                }],
                yAxes: [{
                    ticks: {
                        suggestedMax: 8,
                        suggestedMin: 0,
                        stepSize: 1,

すると、以下のようにいい感じに収まります。
7月7日は[2,0,0,0,0,0,0]なので、積み上げしても2+0+0+0+0+0+0+0=2で値は変わらないですよね。
そして、肝心の凡例ですがきちんと対応した日に何をしたか名称が出てきています。

ここまでで、凡例を7つ作成する理由とリスト化したdataの動きが分かっていただけたでしょうか。
なんとなくでもイメージ掴めたのでしたら幸いです。

さて、ここからはいよいよmodelから取り出したデータを変数に格納します。
改めてviews.pyのコードを見てみましょう。コードの良い悪いは一旦見逃してください。

#棒グラフ用データ

def index(request):
    category=[]
    time=[]
    day=[]

    queryset = leraning_time.objects.all().order_by('-id')[:7]
    for Leraning_time in queryset:
        category.append(Leraning_time.category)
        day.append(str(Leraning_time.day))
        time.append(Leraning_time.time)

    T1 = [0, 0, 0, 0, 0, 0]
    T2 = [0, 0, 0, 0, 0, 0]
    T3 = [0, 0, 0, 0, 0, 0]
    T4 = [0, 0, 0, 0, 0, 0]
    T5 = [0, 0, 0, 0, 0, 0]
    T6 = [0, 0, 0, 0, 0, 0]
    T7 = [0, 0, 0, 0, 0, 0]

    if len(time) > 0:
        T1.insert(0, time[0])
        T2.insert(1, time[1])
        T3.insert(2, time[2])
        T4.insert(3, time[3])
        T5.insert(4, time[4])
        T6.insert(5, time[5])
        T7.insert(6, time[6])

    return render(request, 'new_learn_mgr/index.html', {
        'C1': category[0],
        'C2': category[1],
        'C3': category[2],
        'C4': category[3],
        'C5': category[4],
        'C6': category[5],
        'C7': category[6],
        'T1': T1,
        'T2': T2,
        'T3': T3,
        'T4': T4,
        'T5': T5,
        'T6': T6,
        'T7': T7,
        'day': day,
    })

最初に各要素を入れるリストを初期化します。後々migrateするときにエラーが出てしまうのを防ぐためです。今はあまり気にしなくて大丈夫です。

def index(request):
category=[]
time=[]
day=[]

ここからが本題です。以下のコードでmodels.pyで定義したテーブルからID降順で7日分のデータを取り出します。
必要なデータは日付、時間、内容の3つだけでいいですね。

queryset = leraning_time.objects.all().order_by(‘-id’)[:7]→データIDが最新のものから7つ拾ってます
for Leraning_time in queryset:
category.append(Leraning_time.category)
day.append(str(Leraning_time.day))
time.append(Leraning_time.time)

次は7つ分のdata用の入れ物を作ります。変数がなんでTなのか忘れました。
多分時間入れるからタイムのTですかね。変数のセンスというか基礎知識のなさが見えてしまいますね。

T1 = [0, 0, 0, 0, 0, 0]
T2 = [0, 0, 0, 0, 0, 0]
T3 = [0, 0, 0, 0, 0, 0]
T4 = [0, 0, 0, 0, 0, 0]
T5 = [0, 0, 0, 0, 0, 0]
T6 = [0, 0, 0, 0, 0, 0]
T7 = [0, 0, 0, 0, 0, 0]

では、続いて、学習した時間をそれぞれの変数にinsertで格納します。

if len(time) > 0:
T1.insert(0, time[0])
T2.insert(1, time[1])
T3.insert(2, time[2])
T4.insert(3, time[3])
T5.insert(4, time[4])
T6.insert(5, time[5])
T7.insert(6, time[6])

さらに、凡例用のcategoryである C1〜C7とT1〜T7をtemplateで使えるようにします。
ちなみにCはcategoryのCだと思います。
dayの日付はそのままリストで取り出せば大丈夫です。

return render(request, ‘new_learn_mgr/index.html’, {
‘C1’: category[0],
‘C2’: category[1],
‘C3’: category[2],
‘C4’: category[3],
‘C5’: category[4],
‘C6’: category[5],
‘C7’: category[6],
‘T1’: T1,
‘T2’: T2,
‘T3’: T3,
‘T4’: T4,
‘T5’: T5,
‘T6’: T6,
‘T7’: T7,
‘day’: day,
})

STEP3:グラフを出力する

最後にtemplateとなるHTMLファイルにchart.jsの記述をします。
通常はここでグラフの値となる数値を指定しますが、今回は常に最新の7つだけ表示させるので、値は変数で指定します。

data: {
labels: {{ day|safe }}, →グラフ下部の日付が入るところ
datasets: [
{
label: ‘{{ C1|safe }}’, →カテゴリ
data: {{ T1|safe }},  →時間
backgroundColor:”rgba(255, 99, 132, 0.2)”,
borderColor:”rgba(255, 99, 132, 1)”

これでグラフが表示されます。
私はつまいづいてしまったので一応書きますが、djangoは {{ }} 二重かっこで変数を展開しますが、クォーテーションをつけないと文字列として認識されません。

以上で、chart.jsの値に変数を使用する方法の解説は終わりです。
かなり悩みましたが、振り返ってみるとviews.pyでの値の取り出しがうまくできれば、あとはchart.jsの書式に当てはめるだけですので、分かってしまえばそこまで難しくないかもしれません。

グラフがびよーんと伸びるのが好きでどうしても使いたかったです。
皆様もぜひお試しください!
コードについては色々課題もあるかと思いますが、グラフを表示させる例としてご覧いただければ幸いです。

タイトルとURLをコピーしました