HTML
jQuery
Flask
matplotlib
python3

Matplotlib描画画像をFlaskによるWebアプリにてBytesIO を介して直接表示する方法

本日は

Matplotlibで作成したグラフをFlaskで作ったWebアプリに埋め込みたい.
タイトルが長々しい.日本語難しい.

調べると多くのReferenceが存在する.

などがあります. 
savefigを使って一度画像として保存してそれを読み込む例が多くみられます.
保存せずに直接行ける方法はないかと探してみたらGitHubで公開している例がありました.

元コードはPython2系のものですが,  tacaswell 氏による Python3 パッチを当てていました.

問題は

サーバーサイドのコードだけあってもどう使うか,どうやってWeb側に渡すんだべ?と悶々としてました. どうやって調べたかは定かではないですが,

をなどを参考にしながら画像データを渡すことにしました.

実装例

一番感心があるところだと思うのでコードを貼り付けましょう.

Python側

#app.py
from flask import Flask, render_template
from io import BytesIO
import urllib
from matplotlib.backends.backend_agg import FigureCanvasAgg
from matplotlib.figure import Figure
import numpy as np

app = Flask(__name__)


@app.route("/plot/<func>")
def plot_graph(func='sin'):

    fig = Figure()
    ax = fig.add_subplot(111)
    xs = np.linspace(-np.pi, np.pi, 100)
    if func == 'sin':
        ys = np.sin(xs)
    elif func == 'cos':
        ys = np.cos(xs)
    elif func == 'tan':
        ys = np.tan(xs)
    else:
        ys = xs
    ax.plot(xs, ys)
    canvas = FigureCanvasAgg(fig)
    png_output = BytesIO()
    canvas.print_png(png_output)
    img_data = urllib.parse.quote(png_output.getvalue())
    return img_data


@app.route("/")
def index():
    return render_template("index.html", img_data=None)

if __name__ == "__main__":
    app.run(debug=True, port=9999)

HTML側

<!--index.html-->
<!DOCTYPE html>
<html>

<head>
    <title>Flask with matplotlib</title>
    <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
    <script type="text/javascript">
    function drawGraph(obj) {
        var idx = obj.selectedIndex;
        var value = obj.options[idx].value;
        var plotdata = document.getElementById('plotimg');
        $.get("/plot/" + value, function(data) {
            plotdata.src = "data:image/png:base64," + data;
        });
    };
    $(document).ready(function() {
        //initialize components
        var target = document.getElementById('selector');
        drawGraph(target);
    });
    </script>
</head>

<body>
    <h1>Plot Graph</h1>
    <div>
        <p>select a function</p>
        <select id=selector onchange="drawGraph(this)">
            <option value="sin">sin</option>
            <option value="cos" selected>cos</option>
            <option value="tan">tan</option>
        </select>
    </div>
    <br/>
    <img id=plotimg></img>
</body>

</html>

HTML内で閉じるように書いています.もちろんJSとファイルを別に分離しても良いです.私はWeb屋ではないのでここら辺は雑になりがちです. 編集リクエストがありましたら順次対応してまいります.

Remark

Flaskをお使いの方はご存知だと思いますが, 
ディレクトリの構造は次のようになります:

.
├── app.py
└── templates
    └── index.html

すなわち, app.py と同階層にtemplates というフォルダを作成してその直下に index.html を配置するというものになります.

振る舞いについて

このコードは外部ライブラリとしてjQueryを用いています.

ブラウザを立ち上げた時に

$(document).ready(function()){}

で定義した挙動が行われます.内部ではdrawGraphが呼び出されます.これはさらに

$.get("/plot/" + value, function(data)

を呼び出し

サーバー側であるPythonのコードを呼び出します:

@app.route("/plot/<func>")
def plot_graph(func='sin'):
   #do something

実行例

$ python app.py

この後ブラウザを立ち上げて 127.0.0.1:9999 にアクセス

Screen Shot 2017-09-26 at 18.37.40.png

selectタグ selector の初期値はvalue=cosとなっているため, plot_graph に渡される引数の値は cos です.

画面左上のセレクタは sin cos tan と選べます.選択するごとに drawGraph がイベントハンドラとして呼び出されるので,対応する関数が描画されます.

最後に...

  • CSS でもっと修飾させればかっこよくなるかも.
  • Webアプリ前提としてグラフを描画する機能を提供するライブラリは Bokeh だったり Dash (というかplotly) があります. Matplotlibだけが全てじゃないので個々の開発者にとって使いやすいものを適宜チョイスしていきませう.