【Django入門】Matplotlibで描いたグラフをAjaxでリアルタイムに変更する方法

※ 当サイトではアフィリエイト広告を利用しています。リンクは広告リンクも含みます。

この記事は約23分で読めます。
広告

Matplotlibで描いたグラフをボタンを押したら
グラフだけが書き換わる方法です。
Ajaxを使うことによりページの一部だけを更新できます。
こちらの記事を参考にプログラムを作りました。
django】Ajaxによる非同期通信:動的にページ更新する方法
この記事では文字表示ですが、それをMatplotlibで描いた図に応用してみました。
効果が分かりやすいように2番目のyの値を±1ボタンで変えられるようにしました。
±1ボタンを押すたびにグラフのみ再描画します。(デプロイしたサーバーRenderだと描画のレスポンスが4秒かかります。無料プランだからか。なので±1ボタンは4秒以内の連続クリックは無視するように処理追加しました。ローカルだと速いです。)

Matplotlibアプリ(Ajax版)お名前.comVPS(有料)にデプロイ
Matplotlibアプリ(Ajax版)Renderデプロイ(表示するまで数分かかることがあります)

この記事の続きになります。

Pyscriptを使うと簡単に同じようなことができます。

Vueを使うと見た目がよくなります。

Streamlitを使うとMatplotlibとの相性がいいです。

広告

Ajaxとは

この記事を書いたころより理解が進んでいるので、こちらの記事もおすすめです。htmlレスポンスによる全体的変化と部分的変化の違いに対する理解が深まるかもしれません。

非同期という言葉がつきまといますが一旦忘れましょう。
非同期はレスポンスが返ってくる間に他のことをするのが目的なので
今回は関係ないと思います。

Ajaxを使ってみて以下のように私はとらえています。

  • レスポンスで返ってくるものは部分的なデータ
  • データ受信したことをきっかけにhtmlを書き換え(Javascriptで処理)して再表示
  • 画面が部分的に書き換わっているように見えるが実際は
    DOM操作によって書き換わったindex.htmlを全部表示しなおしてる。
  • 書き換える際に白い画面が表示する時間が一瞬なので
    部分的に書き換わっているように見える。
  • 白い画面はプログラムでどうこうできるわけはなく
    プラウザまたはパソコンの標準機能
  • 画面をクリアしなければ(白い画面を出さなければ)残像が残ってしまう。

Matplotlibアプリ(Ajax版)Railwayデプロイ(毎月21日以降は停止してます)
Matplotlibアプリ(Ajax版)Renderデプロイ(表示するまで数分かかることがあります)でも、
Windowsの場合F5(再読み込み)すれば白い画面が一瞬出ます。
文字は消えてないように見えるだけで実際は一瞬消えてると思います。

Ajax解説図(独自解釈) 2ページあります。>で次ページ。

グラフを更新する方法

【Django入門】データ入力してグラフを描画する方法【Matplotlib】では
以下のようなurlにアクセスすることで描画していました。

<img src="{% url 'mtplt_app:plot' %}" width=600 height=600>

今回も基本的に同じです。
Ajax通信でレスポンスを受け取ったところ(赤)で
青色をDOM操作で書き換えています。

          <div class="result">
            <p>Ajaxあり</p>
            <img src="{% url 'mtaja_app:plot' %}" width=300 height=300>
          </div>

略

            .done(function(response){
                 $('.result').children('img').attr('src', "{% url 'mtaja_app:plot' %}" );
            }); 

しかし見てわかる通り書き換えているといっても
まったく同じコードに書き換えています。
こうなるとプラウザはindex.htmlに変化がないものとして扱い
キャッシュにある画像を表示してしまい更新されません。

それを避ける方法です。
URLが同じ名前にならないように以下の赤部分のように
「?今の時間」というURLクエリを追加します。
そうすれば毎回違うURLになります。
この部分はurls.pyに行っても無視されるので問題ありません。
プラウザに違うURLの画像ですということを知らせるための仕掛けです。

           .done(function(response){
                 $('.result').children('img').attr('src', "{% url 'mtaja_app:plot' %}"+ '?' + new Date().getTime() );
            }); 

目次へ

コード

コードのみです。
下の記事を参考にしてください。
替えたところ
プロジェクト名 matplotlib_app → matplotlib_ajax2
アプリ名 mtplt_app → mtaja_app
settings.pyのINSTALLED_APPS = [に追加するのは
‘mtaja_app.apps.MtajaAppConfig’,になります。

Ajaxの部分の処理は入力値の変更や+1、-1ボタンの情報を一旦Djangoに送り、セッションへ保存。そのレスポンスを受信して改めてimgによってDjangoへグラフをプロットするリクエストを送っています。なので2回リクエストしています。

urls.py

プロジェクト(matplotlib_ajax2)のurls.py

from django.contrib import admin
from django.urls import path,include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('',include("mtaja_app.urls")),
]

アプリ(mtaja_app)のurls.py

from django.urls import path
from . import views

# app_name = 'blog'
app_name = 'mtaja_app'
urlpatterns = [
    path('', views.MatplotlibView.as_view(), name="index"),
    path('plot/', views.get_svg, name='plot'),
    path('ajax-number/', views.ajax_number, name='ajax_number'),
    path('ajax-numberp/', views.ajax_numberp, name='ajax_numberp'),
    path('ajax-numberm/', views.ajax_numberm, name='ajax_numberm'),
]

views.py

from django.shortcuts import render
from django.conf import settings
from django.http import JsonResponse

def index(request):
    return render(request, 'mtpltaj_app/index.html', {})

def ajax_number(request):

    number1 = request.POST.get('number1')
    number2 = request.POST.get('number2')
    mode = request.POST.get('mode')
    #セッションクリア
    request.session.clear()
    #セッション&クッキー 削除
    request.session.flush()
    request.session['plusbtn']='OFF'
    request.session['minusbtn']='OFF'
    y=number2.split(",")
    y=list(map(int, y))
    request.session['delta']=y[1]
    request.session['maxy']=max(y)
    request.session['miny']=min(y)
    if number1 !="" and number2 !="":

        request.session['xvalue']=number1
        request.session['yvalue']=number2
        request.session['mode']=mode

    d = {
        'y2' : y[1],
    }
    return JsonResponse(d)
    
def ajax_numberp(request): 
    plusbtn = request.POST.get('plusbtn')
    delta=request.session['delta']+1
    if delta >= int(request.session['maxy']):
        delta=request.session['maxy']

    request.session['plusbtn']=plusbtn
    request.session['delta']=delta
    d = {
        'y2' : delta,
    }
    return JsonResponse(d)
def ajax_numberm(request): 
    minusbtn = request.POST.get('minusbtn')
    delta=request.session['delta']-1
    if delta <= int(request.session['miny']):
        delta=request.session['miny']
    request.session['minusbtn']=minusbtn
    request.session['delta']=delta
    d = {
        'y2' : delta,
    }
    return JsonResponse(d)   
    
    
import matplotlib
#バックエンドを指定
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import io
from django.http import HttpResponse

from django.shortcuts import render
#クラスベースのビューを作るため
from django.views import View

#Viewを継承してGET文、POST文の関数を作る
class MatplotlibView(View):

    def get(self, request, *args, **kwargs):
        return render(request,"mtpltaj_app/index.html")

    def post(self, request, *args, **kwargs):

        return render(request,"mtpltaj_app/index.html")
        
#グラフ作成
def setPlt(x,y,mode):

    fig, ax = plt.subplots(facecolor='white') 
    if mode =="plot":
        ax.plot(x,y)
        plt.title("X-Y plot")
    elif mode == "bar":
        ax.bar(x,y)
        plt.title("X-Y var")
    plt.xlabel("X")
    plt.ylabel("Y")
# SVG化
def plt2svg():
    buf = io.BytesIO()
    plt.savefig(buf, format='svg', bbox_inches='tight')
    s = buf.getvalue()
    buf.close()
    return s

# 実行するビュー関数
def get_svg(request):
    plusbtn=request.session['plusbtn']
    minusbtn=request.session['minusbtn']
    delta=int(request.session['delta'])
    x1=request.session['xvalue']
    y1=request.session['yvalue']

    if plusbtn == 'ON'or minusbtn == 'ON':
        x=x1.split(",")
        x=list(map(int, x))
        y2=y1.split(",")
        y = [int(n) for n in y2]
        y[1]=delta

    else:
        x=x1.split(",")
        x=list(map(int, x))
        y=y1.split(",")
        y=list(map(int, y))
    request.session['plusbtn']='OFF'
    request.session['minusbtn']='OFF'
    request.session['delta']=delta
    mode=request.session['mode']
    setPlt(x,y,mode)  
    svg = plt2svg()  #SVG化
    plt.cla()  # グラフをリセット
    response = HttpResponse(svg, content_type='image/svg+xml')
    return response

目次へ

Templates

Ajaxの部分は前半はcsrfをクッキーから取り出して送っています。これがないとDjangoが受け付けません。csrfについては、こちらも参考にしてください。

result.html→index.htmlに名前を変更しています。
ローカルでやるときは±1ボタンを連続クリックできないようにしている時間設定値を4000→500に変更することで4秒から0.5秒へ短縮できます。これ以上短くすると連続クリックしたとき動作がおかしくなります。

<!doctype html>
<html lang="ja">
  <head>
    <title>Ajax</title>
  </head>
  <body>
      <div class="container">
          <h2>Matplotlibアプリ(Ajax版)</h2>
          <p>XとYにそれぞれカンマ(,)区切りでデータを入力→グラフタイプを選択して描画を押せば表示します。</p>
          <form id="ajax-number" action="{% url 'mtpltaj_app:ajax_number' %}" method="POST">
              {% csrf_token %}

     		<label>X: 
            	<input type="text" value="" id="xvalue" name="xvalue"> 例1,2,3,4,5,6,7</label>
		    <br>
		    <label>Y: 
        	    <input type="text" value="" id="yvalue" name="yvalue"> 例10,20,30,40,50,60,70</label>

    		<br>
	    	<input type="radio" id="mode" name="mode" value="plot" checked="checked">折れ線
		    <input type="radio" id="mode" name="mode" value="bar">棒グラフ
    		<br>
              <button type="submit" >描画</button>
          </form>
          <p>2個目の要素を下のボタンで増減(他のyの値の範囲内)
          <p></p>y2:<span class="y2">0</span></p>
          <button id="buttonup">+1</button>
          <button id="buttondown">-1</button>
          <span>連続押し防止のため4秒以内での連続押しは無視しています。</span>

          <div class="result">
            <p>Ajaxあり</p>
            <img src="{% url 'mtpltaj_app:plot' %}" width=300 height=300>
          </div>
             <p>Ajaxなし(ページ更新しないと表示されません)</p>
            <img src="{% url 'mtpltaj_app:plot' %}" width=300 height=300>
      </div>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script>
        function getCookie(name) {
            var cookieValue = null;
            if (document.cookie && document.cookie !== '') {
                var cookies = document.cookie.split(';');
                for (var i = 0; i < cookies.length; i++) {
                    var cookie = jQuery.trim(cookies[i]);
                    // Does this cookie string begin with the name we want?
                    if (cookie.substring(0, name.length + 1) === (name + '=')) {
                        cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                        break;
                    }
                }
            }
            return cookieValue;
        }

        var csrftoken = getCookie('csrftoken');

        function csrfSafeMethod(method) {
            // these HTTP methods do not require CSRF protection
            return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
        }

        $.ajaxSetup({
            beforeSend: function (xhr, settings) {
                if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
                    xhr.setRequestHeader("X-CSRFToken", csrftoken);
                }
            }
        });

        $('#ajax-number').on('submit', function(e) {
            e.preventDefault();

            $.ajax({
                'url': '{% url "mtpltaj_app:ajax_number" %}',
                'type': 'POST',
                'data': {
                    'number1': $('#xvalue').val(),
                    'number2': $('#yvalue').val(),
                    'mode': $('input[name=mode]:checked').val(),
                },
                'dataType': 'json'
            })
            .done(function(response){
                $('.y2').text(response.y2);
                $('.result').children('img').attr('src', "{% url 'mtpltaj_app:plot' %}"+ '?' + new Date().getTime() );
            });
        });
        
        var isProcessing = false;

        $('#buttonup').on('click', function() {
            if (!isProcessing) {
                isProcessing = true;

                $.ajax({
                    'url': '{% url "mtpltaj_app:ajax_numberp" %}',
                    'type': 'POST',
                    'data': {
                        'plusbtn': 'ON',
                    },
                    'dataType': 'json'
                })
                .done(function(response){
                    $('.y2').text(response.y2);
                    $('.result').children('img').attr('src', "{% url 'mtpltaj_app:plot' %}"+ '?' + new Date().getTime() );
                    setTimeout(function() {
                        isProcessing = false;
                    }, 4000); // 4秒後に処理を再開
                });
            }
        });

        $('#buttondown').on('click', function() {
            if (!isProcessing) {
                isProcessing = true;

                $.ajax({
                    'url': '{% url "mtpltaj_app:ajax_numberm" %}',
                    'type': 'POST',
                    'data': {
                        'minusbtn': 'ON',
                    },
                    'dataType': 'json'
                })
                .done(function(response){
                    $('.y2').text(response.y2);
                    $('.result').children('img').attr('src', "{% url 'mtpltaj_app:plot' %}"+ '?' + new Date().getTime() );
                    setTimeout(function() {
                        isProcessing = false;
                    }, 4000); // 4秒後に処理を再開
                });
            }
        });

    </script>
  </body>
</html>

Renderで実行したとき、どこが遅いのか

関数の処理時間を計測(ラッパー追加)しprintしてRenderのLogで確認してみました。
SVG化の処理plt2svgがローカルでやった時より極端に遅れていて、この処理のせいでレスポンスが4秒かかってます。
Render
Aug 23 09:24:53 AM 処理時間: setPlt: 0.130700s
Aug 23 09:24:53 AM 処理時間: plt2svg: 3.101816s
ローカル
処理時間: setPlt: 0.29991s
処理時間: plt2svg: 0.145003s

#関数の処理時間計測ラッパー
import datetime
from functools import wraps
def calc_func_time(f):
    @wraps(f)
    def wrapper(*args, **kwargs):
        start = datetime.datetime.now()
        result = f(*args, **kwargs)
        end = datetime.datetime.now()
        print(f'処理時間: {f.__name__}: {(end - start).seconds}.{(end - start).microseconds}s')
        return result
    return wrapper

略
@calc_func_time
def setPlt(x,y,mode):
略
@calc_func_time
def plt2svg():

目次へ
MENTAやってます(ichige)
イチゲをOFUSEで応援する


コメント

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