Djangoでmatplotlibによるグラフ表示する方法
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 %}
実行例
沖縄の詳細画面を開くと、「東京」へのリンクが表示されます。
東京の詳細画面を開くと、「沖縄」へのリンクが表示されます。
こんにちは、突然コメントにて失礼致します。
最近djangoとpythonを勉強し始めた初心者です。
日本語の貴重な情報を公開していただき、ありがとうございます。
いつも参考にさせていただいています。
djangoのDBにデータを登録して、さまざまなグラフを作成したいと考えています。
matplotlibを使ったグラフの表示大変参考になるんですが、sqlite3のDBに登録されているデータを使って
グラフを作成する方法はどのようにしたらできるのでしょうか。
可能でしたら、ご教示いただけませんでしょうか。
よろしくお願い致します。
コメントありがとうございます。
当該記事に「DBからデータを取得してグラフ描画」という項目を追記いたしました。
グラフについてはjqueryを用いると綺麗なグラフが描画できるので、以下の記事も参考にしてみてください。
https://intellectual-curiosity.tokyo/2018/10/31/django%E3%81%A7jquery%E3%81%AB%E3%82%88%E3%82%8B%E3%82%B0%E3%83%A9%E3%83%95%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95/
早速項目を追加していただきありがとうございます。
参考にさせていただきます。
まだまだ初心者でいろいろと勉強することが多いですが、
一つ一つ学習していきたいと思います。
ありがとうございます。
こんにちは。
ご指導いただいたグラフの描画についていろいろと改良をしようとしていますが
なかなかうまく進まない状況です。
今取り組んでいきたいと考えているのは、以下の2つです。
①csvデータから一度にデータを追加
②複数のグラフを一度に描画して詳細画面に表示
についてもし可能であればヒントなどご教示いただけませんでしょうか。
以下のような感じでいかがでしょうか。
①csvデータから一度にデータを追加について
Pythonで実行する場合
with open(FILE_NAME, newline=”) as file:で対象csvファイルを開いて
reader = csv.reader(file)でcsv読み込み後、データをDBに登録すれば出来ます。
別途、記事を投稿したいと思います。
②複数のグラフを一度に描画して詳細画面に表示
matplotlibを用いた複数グラフ描画にいては、当該記事に追記いたしました。
jqueryを用いて詳細画面に複数グラフを描画する方法は、以下の記事が参考になるかと思います。
DjangoでAjaxによるデータ更新を行う方法
https://intellectual-curiosity.tokyo/2018/11/02/django%E3%81%A7ajax%E3%81%AB%E3%82%88%E3%82%8B%E3%83%87%E3%83%BC%E3%82%BF%E6%9B%B4%E6%96%B0%E3%82%92%E8%A1%8C%E3%81%86%E6%96%B9%E6%B3%95/
<ポイント>
detail.htmlにて以下のタグでグラフ描画htmlを読み込んでいます。
{% include ‘monitor/chart.html’ %}
<追加の例>
例えば、もう一つグラフを詳細画面に追加したい場合
・urls.pyに以下のようなパスを追加
path(“monitor//char2t/”, views.draw_chart, name=’chart2′),
・views.pyにdraw_chartを追加
(return render(request, ‘monitor/chart2.html’, {‘y_data’: y_data, ‘x_data’: x_data})みたいな感じ)
・templateにmonitor/chart2.htmlを追加
・detail.htmlに以下のようなタグを追加
{% include ‘monitor/chart2.html’ %}
csvファイルからDB(sqlite3)にレコードを登録する方法の記事を投稿いたしました。
PythonでcsvファイルからDBにレコードを登録する方法
https://intellectual-curiosity.tokyo/2018/12/16/python%E3%81%A7csv%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%8B%E3%82%89db%E3%81%AB%E3%83%AC%E3%82%B3%E3%83%BC%E3%83%89%E3%82%92%E7%99%BB%E9%8C%B2%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95/
まさにやりたいことが記載されておりました。
非常にわかりやすい記事です。本当にありがとうございます。
自分がやりたい事のドンピシャの内容が説明されていてとても助かりました。
1点教えて頂きたいのですが、各ローケーション上記だと沖縄と東京があると思うのですが、
それぞれの関連するURL(他のウェブページ)を各html(東京と沖縄)に複数追記( href=”)したい場合どのようにすればよろしいでしょうか?
どうしてもそのやり方が分からず、困っております。お手数おかけしますが、お願い致します。
現在、views.pyのDetailViewのクラスに2つのmodelを設定できないか調べているのですが、それもできそうになく...
意図している内容と合致しているかは分かりませんが、「各詳細画面で関連するページへのリンクを表示する方法」を当該記事に追記いたしました。
上述していますが、以下の実装で実現可能です。
・views.py
・DetailViewクラスのcondextに、Locationデータ一覧を渡す。
・detail.html
・views.pyで渡したLocationデータをforループでリンク表示する。