Djangoでmatplotlibによるグラフ表示する方法をご紹介します。
目次
条件
- Django 2.1.2
- Python 3.7.0
urls.pyの設定
トップ画面に一覧画面を表示し、詳細画面にグラフを表示するという動作とします。
詳細画面の他に「グラフ描画」の画面を定義します。
グラフの描画処理はview.pyのget_svg関数で実施します。
from django.urls import path from . import views app_name = 'monitor' urlpatterns = [ # トップ画面 path('', views.IndexView.as_view(), name='index'), # 詳細画面 path('monitor/<int:pk>/', views.DetailView.as_view(), name='detail'), # グラフ描画 path('monitor/<int:pk>/plot/', views.get_svg, name='plot'), ]
views.pyの設定
matplotlibとnumpyをインポートします。
HttpResponse(svg, content_type=’image/svg+xml’)でsvg形式で出力します。
from django.http import HttpResponse from django.views import generic from .models import Location, Greenhouse from django.contrib.auth.decorators import login_required from django.contrib.auth.mixins import LoginRequiredMixin import io import matplotlib.pyplot as plt import numpy as np class IndexView(LoginRequiredMixin, generic.ListView): # generic.ListViewを継承 model = Location paginate_by = 5 ordering = ['-updated_at'] template_name = 'monitor/index.html' class DetailView(generic.DetailView): model = Location template_name = 'monitor/detail.html' # グラフ作成 def setPlt(pk): # 折れ線グラフを出力 # TODO: 本当はpkを基にしてモデルからデータを取得する。 x = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) y = np.array([20, 90, 50, 30, 100, 80, 10, 60, 40, 70]) plt.plot(x, y) # svgへの変換 def pltToSvg(): buf = io.BytesIO() plt.savefig(buf, format='svg', bbox_inches='tight') s = buf.getvalue() buf.close() return s def get_svg(request, pk): setPlt(pk) # create the plot svg = pltToSvg() # convert plot to SVG plt.cla() # clean up plt so it can be re-used response = HttpResponse(svg, content_type='image/svg+xml') return response
detail.htmlの設定
「img src」でグラフ描画用のURLを指定します。
パラメータとしてモデルのプライマリーキー「object.pk」を渡すことで、詳細画面ごとのグラフを表示するようにします。
{% extends 'base.html' %} {% block content %} <h1>{{ object.name }}</h1> <section class="post-text"> {{ object.memo|linebreaksbr }} </section> <section class="post-date"> <p>Created: {{ object.created_at }}<span>/</span>Updated: {{ object.updated_at }}</p> </section> <img src='{% url 'monitor:plot' object.pk %}' width=600 height=600> <section> <p><a href="javascript:history.back()">< Back</a></p> </section> {% endblock %}
表示例
詳細画面を開くと、以下のように表示されます。
DBからデータを取得してグラフ描画
DBからデータを取得してグラフ描画のサンプルです。
モデル
場所:気象データ = 1:N になるようモデルを作成します。
# models.py from django.db import models from django.urls import reverse from datetime import datetime as dt class Location(models.Model): """場所モデル""" class Meta: db_table = 'location' name = models.CharField(verbose_name='ロケーション名', max_length=255) memo = models.CharField(verbose_name='メモ', max_length=255, default='', blank=True) author = models.ForeignKey( 'auth.User', on_delete=models.CASCADE, ) created_at = models.DateTimeField(verbose_name='登録日時', auto_now_add=True) updated_at = models.DateTimeField(verbose_name='更新日時', auto_now=True) def __str__(self): return self.name @staticmethod def get_absolute_url(self): return reverse('monitor:index') class WeatherData(models.Model): """気象データモデル""" class Meta: db_table = 'weather_data' unique_together = (('location', 'data_datetime'),) location = models.ForeignKey(Location, verbose_name='ロケーション', on_delete=models.PROTECT) data_datetime = models.DateTimeField(verbose_name='データ日時', default=dt.strptime('2001-01-01 00:00:00', '%Y-%m-%d %H:%M:%S')) temperature = models.FloatField(verbose_name='気温') humidity = models.FloatField(verbose_name='湿度') created_at = models.DateTimeField(verbose_name='登録日時', auto_now_add=True) updated_at = models.DateTimeField(verbose_name='更新日時', auto_now=True) def __str__(self): return self.location.name + ":" + str(self.data_datetime)
ビュー
setPlt(pk)において、WeatherDataからpkのデータ一覧を取得してグラフ描画処理へ渡します。
from django.http import HttpResponse from django.views import generic from .models import Location, WeatherData from django.contrib.auth.mixins import LoginRequiredMixin import io import matplotlib.pyplot as plt class IndexView(LoginRequiredMixin, generic.ListView): model = Location paginate_by = 5 ordering = ['-updated_at'] template_name = 'monitor/index.html' class DetailView(generic.DetailView): model = Location template_name = 'monitor/detail.html' # グラフ作成 def setPlt(pk): # 折れ線グラフを出力 weather_data = WeatherData.objects.select_related('location').filter(location_id=pk) # 対象ロケーションの気象データを取得 # weather_data = WeatherData.objects.raw('SELECT * FROM weather_data WHERE location_id = %s', str(pk)) # このクエリでもOK x = [data.data_datetime for data in weather_data] # 日時 y1 = [data.temperature for data in weather_data] # 気温 y2 = [data.humidity for data in weather_data] # 湿度 plt.plot(x, y1, x, y2) # svgへの変換 def pltToSvg(): buf = io.BytesIO() plt.savefig(buf, format='svg', bbox_inches='tight') s = buf.getvalue() buf.close() return s def get_svg(request, pk): setPlt(pk) # create the plot svg = pltToSvg() # convert plot to SVG plt.cla() # clean up plt so it can be re-used response = HttpResponse(svg, content_type='image/svg+xml') return response
DBデータ
以下のようなレコードが入っているものとします。
場所(Location)
気象データ
動作
一覧画面
詳細画面
複数のグラフを描画する
matplotlibを用いて複数のグラフを描画する場合、axes()やadd_subplot()を用います。
例)横に3つのグラフを並べる場合
# views.py # グラフ作成 def setPlt(pk): # 折れ線グラフを出力 weather_data = WeatherData.objects.select_related('location').filter(location_id=pk) # 対象ロケーションの気象データを取得 # weather_data = WeatherData.objects.raw('SELECT * FROM weather_data WHERE location_id = %s', str(pk)) # このクエリでもOK x = [data.data_datetime for data in weather_data] # 日時 y1 = [data.temperature for data in weather_data] # 気温 y2 = [data.humidity for data in weather_data] # 湿度 # 横に3つのグラフを並べる:axes([左, 下, 幅, 高さ]) plt.axes([0.5, 0.5, 1.0, 1.0]) # 1つ目のグラフ plt.plot(x, y1, x, y2) plt.axes([1.7, 0.5, 1.0, 1.0]) # 2つ目のグラフ plt.plot(x, y1) plt.axes([2.9, 0.5, 1.0, 1.0]) # 3つ目のグラフ plt.plot(x, y2)
<!-- detail.html --> {% extends 'base.html' %} {% block content %} <h1>{{ object.name }}</h1> <section class=""> {{ object.memo|linebreaksbr }} </section> <img src='{% url 'monitor:plot' object.pk %}' width=1500 height=400> <section> <p><a href="javascript:history.back()">< Back</a></p> </section> {% endblock %}
例)縦に3つのグラフを並べる場合
# views.py # グラフ作成 def setPlt(pk): # 折れ線グラフを出力 weather_data = WeatherData.objects.select_related('location').filter(location_id=pk) # 対象ロケーションの気象データを取得 # weather_data = WeatherData.objects.raw('SELECT * FROM weather_data WHERE location_id = %s', str(pk)) # このクエリでもOK x = [data.data_datetime for data in weather_data] # 日時 y1 = [data.temperature for data in weather_data] # 気温 y2 = [data.humidity for data in weather_data] # 湿度 # 縦に3つのグラフを並べる:axes([左, 下, 幅, 高さ]) plt.axes([0.5, 2.4, 1.0, 1.0]) # 1つ目のグラフ plt.plot(x, y1, x, y2) plt.axes([0.5, 1.2, 1.0, 1.0]) # 2つ目のグラフ plt.plot(x, y1) plt.axes([0.5, 0.0, 1.0, 1.0]) # 3つ目のグラフ plt.plot(x, y2)
<!-- detail.html --> {% extends 'base.html' %} {% block content %} <h1>{{ object.name }}</h1> <section class=""> {{ object.memo|linebreaksbr }} </section> <img src='{% url 'monitor:plot' object.pk %}' width=400 height=1000> <section> <p><a href="javascript:history.back()">< Back</a></p> </section> {% endblock %}
上記画像は3つ目のグラフが切れてしまっていますが、画面をスクロールすれば正しく表示されます。
例)2×2のレイアウトに配置する場合
# views.py # グラフ作成 def setPlt(pk): # 折れ線グラフを出力 weather_data = WeatherData.objects.select_related('location').filter(location_id=pk) # 対象ロケーションの気象データを取得 # weather_data = WeatherData.objects.raw('SELECT * FROM weather_data WHERE location_id = %s', str(pk)) # このクエリでもOK x = [data.data_datetime for data in weather_data] # 日時 y1 = [data.temperature for data in weather_data] # 気温 y2 = [data.humidity for data in weather_data] # 湿度 # 2×2のレイアウトに配置する fig = plt.figure(figsize=(15, 10)) row = 2 col = 2 fig.add_subplot(row, col, 1) plt.plot(x, y1, x, y2) fig.add_subplot(row, col, 3) plt.plot(x, y1) fig.add_subplot(row, col, 4) plt.plot(x, y2)
<!-- detail.html --> {% extends 'base.html' %} {% block content %} <h1>{{ object.name }}</h1> <section class=""> {{ object.memo|linebreaksbr }} </section> <img src='{% url 'monitor:plot' object.pk %}' width=1200 height=800> <section> <p><a href="javascript:history.back()">< Back</a></p> </section> {% endblock %}
ソース一式
サンプルソースをGitHubに公開しています。
https://github.com/kzmrt/graph
参考(追記)
各詳細画面から、別の詳細画面へのリンクを表示するようにしてみます。
ポイント
- views.py
- DetailViewクラスのcondextに、Locationデータ一覧を渡す。
- detail.html
- views.pyで渡したLocationデータをforループで<a href>リンク表示する。
(以下の例では、自分のリンクは作成しないようにしています。)
- views.pyで渡したLocationデータをforループで<a href>リンク表示する。
views.py
class DetailView(generic.DetailView): model = Location template_name = 'monitor/detail.html' def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['locations'] = Location.objects.all() return context
detail.html
{% extends 'base.html' %} {% block content %} <h1>{{ object.name }}</h1> <section class=""> {{ object.memo|linebreaksbr }} </section> <img src='{% url 'monitor:plot' object.pk %}' width=1200 height=800> <section> <ul> {% for location in locations %} {% if not location.pk == object.pk %} <li> <h2><a href="{% url 'monitor:detail' location.pk %}">{{ location.name }}のデータ</a></h2> </li> {% endif %} {% endfor %} </ul> </section> <section> <p><a href="javascript:history.back()">< Back</a></p> </section> {% endblock %}
実行例
沖縄の詳細画面を開くと、「東京」へのリンクが表示されます。
東京の詳細画面を開くと、「沖縄」へのリンクが表示されます。