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:&nbsp;{{ object.created_at }}<span>/</span>Updated:&nbsp;{{ object.updated_at }}</p>
</section>

<img src='{% url 'monitor:plot' object.pk %}' width=600 height=600>

<section>
    <p><a href="javascript:history.back()">&lt; 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()">&lt; 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()">&lt; 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()">&lt; Back</a></p>
    </section>
{% endblock %}

ソース一式

サンプルソースをGitHubに公開しています。

https://github.com/kzmrt/graph

Django

参考(追記)

各詳細画面から、別の詳細画面へのリンクを表示するようにしてみます。

ポイント

  • views.py
    • DetailViewクラスのcondextに、Locationデータ一覧を渡す。
  • detail.html
    • 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()">&lt; Back</a></p>
    </section>
{% endblock %}

実行例

沖縄の詳細画面を開くと、「東京」へのリンクが表示されます。

東京の詳細画面を開くと、「沖縄」へのリンクが表示されます。

Djangoでmatplotlibによるグラフ表示する方法” に対して11件のコメントがあります。

  1. 川田文彦 より:

    こんにちは、突然コメントにて失礼致します。

    最近djangoとpythonを勉強し始めた初心者です。

    日本語の貴重な情報を公開していただき、ありがとうございます。
    いつも参考にさせていただいています。

    djangoのDBにデータを登録して、さまざまなグラフを作成したいと考えています。

    matplotlibを使ったグラフの表示大変参考になるんですが、sqlite3のDBに登録されているデータを使って
    グラフを作成する方法はどのようにしたらできるのでしょうか。

    可能でしたら、ご教示いただけませんでしょうか。

    よろしくお願い致します。

    1. 確認飛行物体 より:

      コメントありがとうございます。
      当該記事に「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/

      1. kawada より:

        早速項目を追加していただきありがとうございます。
        参考にさせていただきます。

        まだまだ初心者でいろいろと勉強することが多いですが、
        一つ一つ学習していきたいと思います。

        ありがとうございます。

  2. kawada より:

    こんにちは。

    ご指導いただいたグラフの描画についていろいろと改良をしようとしていますが
    なかなかうまく進まない状況です。

    今取り組んでいきたいと考えているのは、以下の2つです。

    ①csvデータから一度にデータを追加
    ②複数のグラフを一度に描画して詳細画面に表示

    についてもし可能であればヒントなどご教示いただけませんでしょうか。

    1. 確認飛行物体 より:

      以下のような感じでいかがでしょうか。

      ①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’ %}

      1. 確認飛行物体 より:

        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/

  3. Guest より:

    まさにやりたいことが記載されておりました。
    非常にわかりやすい記事です。本当にありがとうございます。

  4. masa より:

    自分がやりたい事のドンピシャの内容が説明されていてとても助かりました。
    1点教えて頂きたいのですが、各ローケーション上記だと沖縄と東京があると思うのですが、
    それぞれの関連するURL(他のウェブページ)を各html(東京と沖縄)に複数追記( href=”)したい場合どのようにすればよろしいでしょうか?
    どうしてもそのやり方が分からず、困っております。お手数おかけしますが、お願い致します。
    現在、views.pyのDetailViewのクラスに2つのmodelを設定できないか調べているのですが、それもできそうになく...

    1. 確認飛行物体 より:

      意図している内容と合致しているかは分かりませんが、「各詳細画面で関連するページへのリンクを表示する方法」を当該記事に追記いたしました。

      上述していますが、以下の実装で実現可能です。
      ・views.py
       ・DetailViewクラスのcondextに、Locationデータ一覧を渡す。
      ・detail.html
       ・views.pyで渡したLocationデータをforループでリンク表示する。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です