Python Django入門 初めの1歩からWEBアプリ作成までの流れ その19 Django X Plotly _ DjangoでPlotlyを使って動的グラフを表示

みなさんこんにちは、ZeroTerasu(@ZeroTerasu)です。

 今回は、Pythonで動的グラフを表示するためのライブラリであるPlotlyを使った、Djangoアプリケーションによる動的グラフブラウザ表示の方法について解説していきます。

 今回の例では、日々の体重を記録するためのアプリを題材にして解説していきます。

作成するアプリの概要

作成するアプリ

今回作成するアプリの各画面構成を紹介します。下記3画面の構成になっています。

  1. トップページ(下記2と3のリンク先のみ表示)
  2. フォームページ(日付毎の体重を入力する画面)
  3. チャートページ(日付毎の体重の動的棒グラフを表示する画面)
作成するアプリ画面1_トップページ

2. フォームページ と 3. チャートページ のリンク先だけが載ったページです。

作成するアプリ画面2_フォームページ

日付と体重を入力するページです。体重は小数点第2位まで入力することができるように設定しています。

作成するアプリ画面3_チャートページ

X軸に日付、Y軸に体重をとる動的チャートを表示するページです。各日付の棒グラフにカーソルを合わせると、日付と体重が数値で個別表示されます。(下図の2023-07-20をご参照下さい。)

今回の記事では、DjangoでPlotlyを使うための方法にフォーカスを当てていきたいと思います。そのため、フォームからモデルを通じてDBにデータを保存する方法(今回の場合、フォームからモデルを通じて日付と体重のデータをDBに保存する方法)についての詳細な解説は下記の記事をご参照下さい。

また、今回のアプリケーションについて、ユーザーからアクセスがあった後の処理手順について解説します。

チャート表示するまでの流れ

①HTTPリクエスト:ブラウザからトップページにアクセス

②pj/urls.pyにリクエスト転送:サーバーソフトが、プロジェクトのurls.pyにリクエストを転送

③app/urls.pyにリクエスト転送:プロジェクトのurls.pyにはトップページへのアクセスがあった際はアプリケーションのurls.pyにリクエストを転送するように設定しているので、リクエストが転送される。

④views.pyにリクエスト転送:アプリケーションのurls.pyには、チャートページにアクセスがあった際のビュークラスとテンプレートが紐づけられており、ビュークラスにリクエストが転送される。

➄app/views.py:リクエストを受け取ったビュークラス(後述のChartView)は、get_context_data関数の中で、create_chart()メソッド(charts.py内で定義)を呼び出します。

⑥, ⑦, ⑧app/charts.py*:charts.py内のcreate_chart()メソッドは、models.py内に定義されているモデル(後述のWeightModel)のデータをmodels.pyに要求します。そして、WeightModelの情報(日付と体重)のデータを元に、動的グラフのHTML(plot_fig)を生成して、views.pyのWeightViewに引き渡します。
*charts.pyというモジュールは、Djangoがデフォルトで用意しているモジュールではありません。charts.pyは、チャート表示用HTMLを生成するためのcreate_chart()メソッド(こちらもDjangoデフォルトで用意されたメソッドではありません。)を格納するためのモジュールとしてオリジナルに作成したモジュールです。つまり、自分で勝手に命名したメソッド(create_chart()メソッド)を自分で勝手に命名した入れ物(charts.py)の中に入れてモジュール(拡張子=”.py”のファイル)として、views.pyから呼び出して使ってもらいやすいようにしたものがcharts.pyです。(そのため、名称は”graph.py”でも”barchart.py”でも何でも構いません。)

➈, ⑩views.py⇔templates:ビュークラスがテンプレートをレンダリング(書き換え)する。その際、テンプレート内のテンプレートタグ({{ form.as_p }}など)を見つけたら、ビュークラスがテンプレートに必要な情報を引き渡してテンプレートを完成させる。

⑪, ⑫views.py⇔ブラウザ:完成されたhtmlファイルがブラウザに引き渡されて、フォーム画面が表示される。

Django X Plotly – ポイント①:charts.py <= Plotlyを用いてモデルのデータからチャートを表示するためのHTMLを生成するモジュール

create_chart()メソッド:動的グラフを表示するためのHTMLを生成するメソッド

charts.pyモジュール:create_chart()メソッドを格納したモジュール。views.pyから呼び出される。

charts.py
import plotly.graph_objects as go
from .models import WeightModel
import pandas as pd
# Create your views here.

def create_chart():
    data = WeightModel.objects.all() 
   # "モデル.objects"の形で取得されるオブジェクトを"QuerySet"と言います。
    # QuesrySetはデータベースから取得した情報を操作することができるインターフェース(窓口役)です。
   # モデルを通じて登録されたレコードは基本的にQuerySetを通じて取得します。
    fig = go.Figure()
    trace = go.Bar(
     # X軸、Y軸にはリスト型またはタプル型の形式で代入する必要があります。
     # values_list()メソッドは、QuerySetから特定のフィールドのみ選択して取得してタプルリスト形式で返します。
        x=list(data.values_list('date', flat=True)), 
        y=list(data.values_list('value', flat=True)),
        )
    fig.add_trace(trace)
    fig.update_layout(xaxis=dict(title='date', tickformat='%Y-%m-%d'),
                  yaxis=dict(title='value', tickmode='auto'), 
                  title='体重推移グラフ')
    plot_fig = fig.to_html(fig, include_plotlyjs=False)
    return plot_fig

Django X Plotly – ポイント②:views.py <= charts.pyのcreate_chart()メソッドを呼び出し、context[“graph”]属性に格納して、テンプレートに引き渡す。

  1. get_contect_date()関数をオーバーライドして、contextに対してcontext[‘graph’]という新たな属性を付加している。
  2. “1”で追加された新たな属性のcontext[‘graph’]には、create_chart()メソッドの結果(Plotlyを用いた動的グラフを生成するためのHTML)が格納される。
  3. contextデータがtemplateに引き渡される。
views.py
from django.views.generic import TemplateView
from .charts import create_chart

class ChartView(TemplateView):
    template_name='app/chart.html'
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['graph'] = create_chart()
        return context

Django X Plotly – ポイント③:chart.html <= テンプレートにviews.pyで生成したcontextを引き渡す。

  1. <meta>タグ内に<script src=”https://cdn.plot.lu/plotly-latest.min.js”></script>を定義
  2. {% autoescape off %}{{ graph }}{% endautoescape %}の形で{{ graph }}を挟む。
  3. {{ graph }}でviews.pyのcontext[“graph”]の値を表示する。
chart.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Chart</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css" integrity="sha384-xOolHFLEh07PJGoPkLv1IbcEPTNtaed2xpHsD9ESMhqIYd0nLMwNLD69Npy4HI+N" crossorigin="anonymous">
    <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
    <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
</head>
<body>
    {% autoescape off %}
    <h2>入力された内容の動的チャートが生成されます。</h2>
    {{ graph }}
    <br>
    <a href="/">トップページへ戻る</a><br>
    <a href="{% url 'app/form' %}">フォームページへ移動する。</a><br>
    {% endautoescape %}
</body>
</html>

ポイントとしては、上記の通りです。以下では、今回作成したアプリの他のモジュールについても参考情報として掲載致します。

pj/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('', include('app.urls')),
    path('admin/', admin.site.urls),
]
pj/app/urls.py
from django.contrib import admin
from django.urls import path
from . import views

urlpatterns = [
    path('', views.Index.as_view(), name='app/index'),
    path('form/', views.WeightView.as_view(), name='app/form'),
    path('chart/', views.ChartView.as_view(), name='app/chart'),
]
app/views.py
from typing import Any, Dict
from django.shortcuts import render
from django.views.generic import TemplateView, CreateView
from .forms import WeightForm
from .models import WeightModel
from .charts import create_chart

# Create your views here.
class Index(TemplateView):
    template_name='app/index.html'

class ChartView(TemplateView):
    template_name='app/chart.html'
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['graph'] = create_chart()
        return context

class WeightView(CreateView):
    template_name='app/form.html'
    form_class=WeightForm
    success_url='/chart'
app/models.py
from django.db import models

# Create your models here.
class WeightModel(models.Model):
    date = models.DateField(verbose_name='日付')
    value = models.DecimalField(verbose_name='体重', decimal_places=2, max_digits=4)
    def __str__(self):
        return str(self.date) + ' ' + str(self.value)
app/forms.py
from django import forms
from .models import WeightModel

class WeightForm(forms.ModelForm):
    class Meta:
        model = WeightModel
        fields = "__all__"

    def __init__(self, *args, **kwargs):
        for field in self.base_fields.values():
            field.widget.attrs["class"]="form-control"
        super().__init__(*args, **kwargs)
pj/templates/app/form.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Form</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css" integrity="sha384-xOolHFLEh07PJGoPkLv1IbcEPTNtaed2xpHsD9ESMhqIYd0nLMwNLD69Npy4HI+N" crossorigin="anonymous">
    <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
</head>
<body>
    <form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <input class="btn btn-primary form-control" type="submit" value="送信">
    </form>
    <a href="/">トップページへ戻る</a><br>
    <a href="{% url 'app/chart' %}">チャートページへ移動する。</a><br>
</body>
</html>
pj/templates/app/index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css" integrity="sha384-xOolHFLEh07PJGoPkLv1IbcEPTNtaed2xpHsD9ESMhqIYd0nLMwNLD69Npy4HI+N" crossorigin="anonymous">
    <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
    <title>トップページ</title>
</head>
<body>
    <br>
    <a href="{% url 'app/form' %}" class="btn btn-info">フォームページへ移動する。</a>
    <br>
    <br>
    <a href="{% url 'app/chart' %}" class="btn btn-primary">チャートページへ移動する。</a>
</body>
</html>
app/admin.py
from django.contrib import admin
from .models import WeightModel

# Register your models here.
admin.site.register(WeightModel)
pj/settings.py
"""
Django settings for pj project.

Generated by 'django-admin startproject' using Django 3.2.6.

For more information on this file, see

Django settings | Django documentation
The web framework for perfectionists with deadlines.
For the full list of settings and their values, see
Settings | Django documentation
The web framework for perfectionists with deadlines.
""" from pathlib import Path # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = 'django-insecure-v#=6@)fkg=e!+y(+y64wliw47oo3g2=iy2r%-v4m$6(8u#vwj0' # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True ALLOWED_HOSTS = [] # Application definition INSTALLED_APPS = [ 'app', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', ] MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] ROOT_URLCONF = 'pj.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [BASE_DIR / 'templates'], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] WSGI_APPLICATION = 'pj.wsgi.application' # Database # https://docs.djangoproject.com/en/3.2/ref/settings/#databases DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': BASE_DIR / 'db.sqlite3', } } # Password validation # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', }, { 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', }, { 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', }, { 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', }, ] # Internationalization # https://docs.djangoproject.com/en/3.2/topics/i18n/ LANGUAGE_CODE = 'en-us' TIME_ZONE = 'UTC' USE_I18N = True USE_L10N = True USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.2/howto/static-files/ STATIC_URL = '/static/' # Default primary key field type # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

コメント

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