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 %}
実行例
沖縄の詳細画面を開くと、「東京」へのリンクが表示されます。
東京の詳細画面を開くと、「沖縄」へのリンクが表示されます。

