djangoをdockerコンテナで利用(3)

◆◆◆

自前DBには計算して表示させる内容とか、blob列にpdfやjpegを入れてる。
そういうのをdjangoから扱うための基礎動作確認ページを作っておこうと思った。

php7.4のコンテナにimagick追加して苦労して表示させてるのを、djangoだと一発で表示できる。円グラフのpng画像が表示できることを目指す。

ubuntu20でdockerコンテナ動かして、django取り扱いやってみるか。

グラフ表示できるようになるまで

グラフを表示させるために、処理を追加するんだけど、表示ができるようになるまでの解説ページがなかなか理解しづらかった。

円グラフ表示できるようになってからは、日本語フォントの扱いがなかなかうまく行かず、2日ぐらいは悩んだ。

それでも、円グラフが縦長表示になる問題が残って、htmlのimg srcの表示をいじった。

次に円グラフに注釈が入ってほしくて、ググって見つけたソースを下敷きにしてやってみた。

何もない状態から、最終的にはこんなグラフが表示できた。

home-graph

djangoでいじった順番

websiteのプロジェクトを作っておいて、gvisDjango3アプリケーションを既に追加してあり、html表示させるためのテンプレートも作ってある。

linuxにdjangoのdockerコンテナ作るとこと、hello worldから始まってDB読むところは別のところにメモった。

以下、いじった順番。vscodeのGitGraph見たらこんな感じやった。

  1. ライブラリをrequirements.txtに追記してグラフ描けるようにする
  2. テンプレートにグラフの画像を描かせる場所を追記する
  3. 画像表示させるリクエストに応答するための追記をする
  4. グラフを描かせる
  5. 微調整

いじった箇所は★の4箇所。

/code/app
|--gvisDjango3
|  |--__init__.py
|  |--__pycache__
|  |--admin.py
|  |--apps.py
|  |--migrations
|  |--models.py
|  |--tests.py
|  |--urls.py ★3
|  |--views.py ★4,5
|--manage.py
|--requirements.txt ★1
|--templates
|  |--gvisDjango3
|  |  |--gvisDjango3Top.html ★2
|--website
|  |--__init__.py
|  |--__pycache__
|  |--asgi.py
|  |--settings.py
|  |--static
|  |  |--admin
|  |--urls.py
|  |--wsgi.py

グラフ表示やってみる

ライブラリをrequirements.txtに追記してグラフ描けるようにする

なんていうライブラリがあるんだろー、からググってmatplotlibでいろいろできそうってことがわかった。

Matplotlib: Python plotting — Matplotlib 3.4.3 documentation

英語ばっかりで苦しいページだけど、やりたいことはほんの少しのことなので、欲しいイメージを見つけたら、その英文を頑張って読む。

コンテナのbashに入ってmatplotlibを追加。
あ、コマンド投入したときのエコーバック捨ててた・・・。
貼り付けはコマンド投入の箇所のみ。

$ docker exec -it docker_sv_django_1 bash
root@svdjango:/# pip3 install matplotlib

requirements.txtにも追記しといた。コンテナ作り直してもこれで何とかなる。

Django==3.2.7
uwsgi==2.0.19.1
django-markdownx==3.0.1
Markdown==3.3.3
Pillow==7.0.0
PyMySQL==1.0.2
matplotlib==3.4.3   ← ここ追記した

テンプレートにグラフの画像を描かせる場所を追記する

テンプレートは既にいくつかの情報を表示させるようにしてある。

「’gvis_graph:plot’をここに埋め込むで」みたいな感じでgvisDjango3Top.htmlに追記 。

「gvis_graph」はurls.pyに書く名前空間につける名前。
「plot」はURLのパスに書く名前。

<p align=left> --- サンプルのグラフ ---</p>
<img src="{% url 'gvis_graph:plot' %}" width=600 height=600 align=middle>
<br>

実際にブラウザ表示させたときのソースはこんな感じになってた。

<p align=left> --- サンプルのグラフを画像で表示 ---</p>
<img src="/gvisDjango3/plot/" width=600 height=600 align=middle>
<br>

画像表示させるリクエストに応答するための追記をする

urls.pyに追記する。

app_nameっていう名前空間を定義しておく。これがないと参照できずにエラーになる。
テンプレートから参照するときの名前らしい。

httpリクエストで「htttp://xxx/plot/」が来たら、viewの中のget_png呼び出せって感じでルーティングしてるみたい。

from django.urls import path

from . import views

app_name = 'gvis_graph'  ## ここ追記

urlpatterns = [
    path('', views.index, name='index'),
    path('plot/', views.get_svg name='plot') ## ここ追記
]

グラフを描かせる

最初に参考にさせてもらったページ。
作者さんありがとう。

MatplotlibのグラフをDjangoで使ってみた
最近Django開発にハマっている筆者ですが、Matplotlibでのグラフ描画に割とハマってしまったので、実装手順を備忘録として残しおきます。 こんな人におすすめ:・DjangoのテンプレートにMatplotlibのグラフを表示させたい。
インポート追記

views.pyの最初のほうに「from xxx」とか書いてる箇所あるので追記。

## グラフ作成のため - ここから
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import io
## グラフ作成のため - ここまで
サンプルの棒グラフ表示

views.pyの最後に追記。

def setPlt():
    # 棒グラフ - ここから
    x = ["07/01", "07/02", "07/03", "07/04", "07/05", "07/06", "07/07"]
    y = [3, 5, 0, 5, 6, 10, 2]
    plt.bar(x, y, color='#00d5ff')
    plt.title(r"$\bf{Running Trend  -2020/07/07}$", color='#3407ba')
    plt.xlabel("Date")
    plt.ylabel("km")
    # 棒グラフ - ここまで

# 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):
    setPlt()  
    svg = plt2svg()  #SVG化
    plt.cla()  # グラフをリセット
    response = HttpResponse(svg, content_type='image/svg+xml')
    return response

表示できたのがこれ。コピペしたとは言っても初めて表示できたときは嬉しかったな。

home-graph
png形式で表示

本来表示したいのは円グラフで、さらにsvgじゃなくpng画像で表示させたい。
まぁ、この辺は好みの問題ってことで。

自分用に書き換え実施。

まずはpngに変更するようviews.pyを更新

# png化
def plt2png():  ←svgじゃないのでpngに名前変更
    buf = io.BytesIO()
    plt.savefig(buf, format='png', dpi=200) ←フォーマットをpngに変更
    s = buf.getvalue()
    buf.close()
    return s

# 実行するビュー関数
def get_png(request): ←svgじゃないのでpngに名前変更
    setPlt()  
    png = plt2png()  #png化 ←plt2pngあるんじゃないかなって想定したらあった
    plt.cla()  # グラフをリセット
    response = HttpResponse(png, content_type='image/png') ←svgじゃないのでpngに変更
    return response

svgをpngに名前変更したので、urls.pyも変更。これで自分用に一歩近づく。

urlpatterns = [
    path('', views.index, name='index'),
    path('plot/', views.get_png, name='plot') ←view.pyの定義はget_svgじゃないのでget_pngに名前変更
]
円グラフを表示

グラフは表示できるようになった。次はグラフの種類を変える。

データ内容は、本利用になったらmodels.pyに定義した内容から取ってくるけど、今はサンプル表示ができればまずはいい。

urls.pyやテンプレートはそのままにして、view.pyを書き換えて円グラフをやってみる。

いくつかあったけど、主に参考にさせてもらったページ。
作者さんありがとう。

Matplotlib - 円グラフの徹底解説!(タイトル、ラベル、凡例、色、フォントサイズ、CSVからの表示) - AI-interのPython3入門
PythonのMatplotlibにおける円グラフの作成方法を初心者向けに解説した記事です。円グラフの描き方や、タイトル、ラベル、凡例、フォントサイズ、色などの装飾方法などを解説しています。

参考ページと同じことするため、ライブラリ追加してからインポートを書き足す。
ここもpip3のエコー捨ててた・・・。

import numpy as np  ## 多次元配列や数値計算のため(要pipインストール 'pip3 install numpy')

views.pyの棒グラフ処理箇所をいったんコメント化して、円グラフやってみる。
残念ながらこのままだとフォントが豆腐になって全角文字が「□」って表示される。
そういえば、何年か前にphpで円グラフ挑戦したときも似たようなことあったっけ。

def setPlt():

    # 棒グラフ - ここから
    '''
    x = ["07/01", "07/02", "07/03", "07/04", "07/05", "07/06", "07/07"]
    y = [3, 5, 0, 5, 6, 10, 2]
    plt.bar(x, y, color='#00d5ff')
    plt.title(r"$\bf{Running Trend  -2020/07/07}$", color='#3407ba')
    plt.xlabel("Date")
    plt.ylabel("km")
    '''
    # 棒グラフ - ここまで

    # (a)要素名の格納
    chart_item = np.array([ "好き", 
                            "嫌い", 
                            "わからん",
                            "ほげほげ", 
                            "doyasa"]
                            )
    # (b)値の格納
    values = np.array([10.5, 20.5, 30.5, 15.5, 25.5])

    # (c)要素毎の色指定
    ## chart_colors = ["r", "c", "b", "m", "y"]
    chart_colors = ["lightpink", "yellow", "gold", "slateblue", "lightcoral"]

    # (d)タイトル
    plt.title("Title - タイトル",
                        fontsize = 20
                        ) 

    # (e)ラベルのフォントサイズ
    plt.rcParams['font.size'] = 16.0

    # (f)グラフの描画
    plt.pie(
            values,  
            counterclock=False,
            startangle=90,
            autopct="%1.1f%%", 
            colors=chart_colors,
            shadow=True,
            wedgeprops={'linewidth': 3, 'edgecolor': 'w'}
            )

    plt.axis('equal')

    # (g)凡例の表示
    plt.legend(chart_item,
                fontsize=10,
                bbox_to_anchor=(0.85,0.7)
                )

    # 日本語フォントを使った円グラフ - ここまで

# png化
def plt2png():
    buf = io.BytesIO()                          # バッファを作成
    plt.savefig(buf, format='png', dpi=200)     # バッファにpngを一時保存
    s = buf.getvalue()                          # バッファの内容を渡す
    buf.close()                                 # バッファはクローズ
    return s

# 実行するビュー関数
def get_png(request):
    setPlt()            # グラフ要素をセット
    png = plt2png()     # グラフをpng化
    plt.cla()           # グラフをリセット
    response = HttpResponse(png, content_type='image/png')
    return response
日本語表示できるようにする

たぶんフォントが入ってないか何かで文字化けしてるんやろなー、って想像しながら日本語表記のための方法を探してみたら、やっぱりフォントの問題。

やったのは、こんな感じ。

  • フォントをdockerコンテナ(ubuntu20)にインストール
  • matplotlibの設定ファイルを書き換え
  • view.pyにそのフォントを指定
フォントをdockerコンテナにインストール

まずはフォントがあるのか。
phpのときはipaのゴシックと明朝使ったことあったっけ。リンク調べたらver4.01に上がってた。

コンテナにbashで入ってaptからそれらしいインストール対象を探してみると、ipaexfontってのがある。なんかWARNINGってあるけど無視。

$ docker exec -it docker_sv_django_1 bash
root@svdjango:/# apt search fonts-* | grep fonts- | grep ipa

WARNING: apt does not have a stable CLI interface. Use with caution in scripts.

fonts-ipaexfont/focal,now 00301-4ubuntu1 all
fonts-ipaexfont-gothic/focal,now 00301-4ubuntu1 all
fonts-ipaexfont-mincho/focal,now 00301-4ubuntu1 all
fonts-ipafont/focal 00303-18ubuntu1 all
fonts-ipafont-gothic/focal 00303-18ubuntu1 all
fonts-ipafont-mincho/focal 00303-18ubuntu1 all
fonts-ipafont-nonfree-jisx0208/focal 1:00103-7 all
fonts-ipafont-nonfree-uigothic/focal 00203-26 all
fonts-ipamj-mincho/focal 005.01-2 all
fonts-sipa-arundina/focal 0.3.2-1 all
latex-fonts-sipa-arundina/focal 0.3.2-1 all
xfonts-tipa/focal 2:1.3-20 all
root@svdjango:/#

コンテナにフォント追加して試させてもらいましょ。
またエコーバック捨ててた・・・。

root@svdjango:/# apt install fonts-ipaexfont-gothic
root@svdjango:/# apt install fonts-ipaexfont-mincho
matplotlibの設定ファイルを書き換え

matplotlibには設定ファイルがあるそうな。
そこにフォントのことを記述してる箇所があるらしい。

参考ページの作者さんありがとう。

matplotlibで日本語 - Qiita
matplotlibで日本語を扱う場合の問題点をまとめてみます。 matplotlibでは既定のフォントが DejaVu Serif になっており、日本語は豆腐になってしまう。英語フォントの足りない部分を日本語フォントにリンクする...
Python matplotlibで日本語の設定方法
Pythonのライブラリmatplotlibはデータを可視化するのに最適なツールですが、初期設定では日本語が対応しておらず、日本語をグラフ内で使用すると「□□□」のような豆腐文字になってしまいます。私もこの日本語設定に手こずってしまったこと

自分でもdjango shellからやってみる。

root@svdjango:/# cd /code/app
root@svdjango:/code/app# /usr/bin/python3 manage.py shell
>>> import matplotlib as mpl
>>> print(mpl.matplotlib_fname())
/usr/local/lib/python3.8/dist-packages/matplotlib/mpl-data/matplotlibrc
>>> exit()
root@svdjango:/code/app#

コンテナの「/usr/local/lib/python3.8/dist-packages/matplotlib/mpl-data/matplotlibrc」が設定ファイルらしいことはわかった。

そこにaptで追加したフォントを書けばいいってことか。
viで開いて更新してみるかな。

root@svdjango:/code/app# vi /usr/local/lib/python3.8/dist-packages/matplotlib/mpl-data/matplotlibrc

けっこう長い定義で、その中のfont.serifって箇所に「DejaVu Serif」から定義が入ってる箇所があるので「IPAexGothic, IPAexMincho,」を追記。
たくさん書いてあったので一部略すけど、こんな感じ。

font.serif:      IPAexGothic, IPAexMincho, DejaVu Serif, Bitstream Vera Serif, (中略) serif

django用コンテナでdjango shellを使ってフォントを認識できているか確認する。

参考ページの作者さんありがとう。

matplotlibで日本語 - Qiita
matplotlibで日本語を扱う場合の問題点をまとめてみます。 matplotlibでは既定のフォントが DejaVu Serif になっており、日本語は豆腐になってしまう。英語フォントの足りない部分を日本語フォントにリンクする...

おお、findSystemFonts()ってやった後の最初の2行に明朝とゴシックが増えてる。

root@svdjango:/# cd /code/app
root@svdjango:/code/app# /usr/bin/python3 manage.py shell
>>> import matplotlib.font_manager as fm
>>> fm.findSystemFonts()
['/usr/share/fonts/opentype/ipaexfont-mincho/ipaexm.ttf',  ←増えてる
'/usr/share/fonts/truetype/fonts-japanese-gothic.ttf',  ←増えてる
'/usr/share/fonts/truetype/dejavu/DejaVuSerif-Bold.ttf', 
'/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf', 
'/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 
'/usr/share/fonts/truetype/fonts-japanese-mincho.ttf', 
'/usr/share/fonts/truetype/dejavu/DejaVuSerif.ttf', 
'/usr/share/fonts/opentype/ipaexfont-gothic/ipaexg.ttf', 
'/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf', 
'/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf']>>>

フォントを追加したり設定を変更したのにうまく反映されない場合があるため、キャッシュの削除ってのがあるらしい。

コンテナでキャッシュ削除をとりあえずやっておく。

root@svdjango:/# cd
root@svdjango:~# cd .cache/
root@svdjango:~/.cache# ls
matplotlib  pip
root@svdjango:~/.cache# cd matplotlib/
root@svdjango:~/.cache/matplotlib# ls
fontlist-v330.json
root@svdjango:~/.cache/matplotlib# rm fontlist-v330.json
root@svdjango:~/.cache/matplotlib# ls -la
total 0
drwxr-xr-x 2 root root  6 Sep 16 06:44 .
drwxr-xr-x 4 root root 35 Sep 14 07:19 ..
root@svdjango:~/.cache/matplotlib#

このへんまで何度もコンテナ上げ下げしたから、どこが要所かわからんくなってるけど、フォントインストールしてmatplotlibの設定変えたら、docker restartせなあかんと思う。

view.pyにフォントを指定

コンテナにフォントが入ったとこまで来たので、次は実際の処理を更新。
円グラフの要素名格納する直前に、フォントを指定しとく。明朝とゴシック見比べたけど、いったんゴシックでやってみよっか。

    # 日本語フォントを使った円グラフ - ここから
    plt.rcParams['font.family'] = 'IPAexGothic' #全体のフォントを設定
    ## plt.rcParams['font.family'] = 'IPAexMincho' #全体のフォントを設定

    # (a)要素名の格納
    chart_item = np.array([ "好き", 
                            "嫌い", 
                            "わからん",
                            "ほげほげ", 
                            "doyasa"]
                            )

やった、日本語表記できてるやん!
でもplt.axis(‘equal’)って書いてるのに、なんか円が細長いなぁ・・・。
しかも、のっぺりしてる。

home-graph

微調整

円グラフの日本語混じり表示はできるようになったけど、細長いのが気になった。
結局、見せ方の問題なのでテンプレートの表記で調整することにした。

<img src="{% url 'gvis_graph:plot' %}" width=600 height=600 align=middle>
<img src="{% url 'gvis_graph:plot' %}" width=800 height=600 align=middle>

widthを少し横に広げて800にし、なんとなく真円に近づけた。

home-graph

吹き出し入りにする

ここまでできたらいっか、と思ったけどやっぱり吹き出しつけたい。
円グラフの要素が小さいときに、無理やりパーセンテージを埋め込まれたら読みにくい。

やってるサンプルないかググってみたら、英語説明やったけどあった。
下のほうの2つめのグラフに吹き出し入ってた。

Labeling a pie and a donut — Matplotlib 3.4.3 documentation

おお、これ改良して日本語表示させたらいけるやん。
やってみたviews.pyがコレ。

#グラフ作成
def setPlt():

    # ドーナツグラフ - ここから
    fig, ax = plt.subplots(figsize=(24,12), subplot_kw=dict(aspect="equal"))

    # (a)要素名の格納
    chart_item = np.array([ "好き",
                            "嫌い",
                            "わからない",
                            "無回答",
                            "ほげほげ",
                            "mogemoge",
                            "もさもさ",
                            "くさくさ",
                            "ころころ",
                            "ぽよぽよ"]
                            )

    # (b)値の格納
    values = np.array([50,2,3,4,7,10,25,48,41,10])

    # (c)ラベルのフォントサイズ
    plt.rcParams['font.size'] = 20.0

    # (d)背景色を設定(値は1まででrgbを指定、淡いグレーにしたかったので0.8を指定)
    fig.set_facecolor((0.8,0.8,0.8))

    # (e)グラフの描画
    wedges, texts = ax.pie(values, wedgeprops=dict(width=0.8), counterclock=False, startangle=90)

    bbox_props = dict(boxstyle="square,pad=0.3", fc="w", ec="k", lw=0.72)
    kw = dict(arrowprops=dict(arrowstyle="-"),
            bbox=bbox_props, zorder=0, va="center")

    # (f)注釈入れて表示、参考URLそのまんま
    for i, p in enumerate(wedges):
        ang = (p.theta2 - p.theta1)/2. + p.theta1
        y = np.sin(np.deg2rad(ang))
        x = np.cos(np.deg2rad(ang))
        horizontalalignment = {-1: "right", 1: "left"}[int(np.sign(x))]
        connectionstyle = "angle,angleA=0,angleB={}".format(ang)
        kw["arrowprops"].update({"connectionstyle": connectionstyle})
        ax.annotate(chart_item[i], xy=(x, y), xytext=(1.35*np.sign(x), 1.4*y),
                    horizontalalignment=horizontalalignment, **kw)

    # (g)タイトルを右端上端にセット(値は1まで)
    ax.set_title("Matplotlib ドーナツグラフタイトル",fontsize = 32,x=1,y=1)

    # (h)凡例の表示
    plt.legend(wedges, chart_item,
                fontsize=20,
                bbox_to_anchor=(0.0,0.7)
                )

    # ドーナツグラフ - ここまで

# png化
def plt2png():
    buf = io.BytesIO()                          # バッファを作成
    plt.savefig(buf, format='png', dpi=200)     # バッファにpngを一時保存
    s = buf.getvalue()                          # バッファの内容を渡す
    buf.close()                                 # バッファはクローズ
    return s

# 実行するビュー関数
def get_png(request):
    setPlt()            # グラフ要素をセット
    png = plt2png()     # グラフをpng化
    plt.cla()           # グラフをリセット
    response = HttpResponse(png, content_type='image/png')
    return response

setPltにさっきのソースを書いて、まずは完全に同じものが表示されることを確認し、
次に自分の入れたい要素と値を書いておく。

日本語表示させて、吹き出しも入れるところまで一発でできた。

forで書いている箇所はsin,cos混じりの吹き出し追記をやってくれてる。
やりたいことができてるので、特に変更せず原本そのまんま。

難しかったのは、タイトルと要素を表示させるポイント数とレイアウトの調整。
テンプレートのimg srcのwidthとheightの数字を上げて、

gvisDjango3Top.htmlのimg srcの箇所は、800と600だったものを1000と500に増やしてる。

<img src="{% url 'gvis_graph:plot' %}" width=1000 height=500 align=middle>

views.pyのfigureの箇所は、6,3だった箇所を24,12に増やしてる。

# ドーナツグラフ - ここから
fig, ax = plt.subplots(figsize=(24,12), subplot_kw=dict(aspect="equal"))

こうしておいて、タイトルと要素なんかの表示ポイント数を上げておく。

理由はわからないけど、表示の微調整やるために書き換えているとfirefoxでうまく表示されないことがあった。仕方ないけどdockerコンテナを都度再起動させてた。

いちいち上げなおさなあかんの面倒やなぁ。

iphoneで開いてみる

動作確認に使う画面全体を開いてみると、ちゃんとグラフも見えてた。
吹き出し箇所があるせいか、グラフが表示されるの2秒ぐらいかかる。

このへんまで来ると、djangoでの習作はほぼ完成。

やっぱり、グラフはのっぺりやな。

home-graph

ここまでの見直し

そういえば手動でpipしてnumpyとかmatplotlibとか入れたし、djangoそのものも新しくしたのにdjangoのバージョンだけでなく、コンテナの名前に”3.1.5″って入ってた(今の最新は3.2.7)。
手順のおさらいもしたいし、一回コンテナ潰してpipとか全部やり直してみるかな。

コメント