Dashに自作htmlを組み込みたい！ ～html埋め込み編～

## 導入
DashはPythonだけでインタラクティブなダッシュボードが作成できるライブラリです。
Pythonは書けるけどhtml／cssは詳しくない、という方でもお手軽にWeb上でグラフ描画などができます。

…という、ぐーぐる先生で検索すると出てくるひととおりのごあいさつは横に置いておきまして。

ここでは、htmlもある程度わかっている人がDashでアプリを作り込み始めて、
「やっぱりグラフ部分以外はhtmlで書きたい」「htmlのメニューを作ってDashコンテンツを埋め込みたい」という欲が出てきた方、あるいは
お手軽に作れるということでお試しでダッシュボードを作ってみたらうっかり上にウケてしまい、「もっとデザインをブラッシュアップしよう！」なんて言われてしまった方（いるのか？）のために、Dashコンテンツとhtmlを共存させる方法について書いていこうと思います。

なお、今回の記事のソースは[Github](https://github.com/ckw-1227/dash_html)に上げておりますので、手っ取り早くサンプルを見たい方はそちらをどうぞ。

## 一般的なDashアプリを準備する
まずはhtmlを埋め込む前のDashアプリを用意します。
そんなに複雑なものを持ってきてもアレなので、[Dash公式ページ](https://dash.plotly.com/)にある[A Minimal Dash App](https://dash.plotly.com/minimal-app)を使うことにします。
まだDashの環境が未整備だという方は[Dash Installation](https://dash.plotly.com/installation)を参考にDash(とpandas)のpip installもお忘れなく。

公式ページにも載っていますが、こちらにも同じソースを載せておきます。
```python
from dash import Dash, html, dcc, callback, Output, Input
import plotly.express as px
import pandas as pd

df = pd.read_csv('https://raw.githubusercontent.com/plotly/datasets/master/gapminder_unfiltered.csv')

app = Dash(__name__)

app.layout = html.Div([
    html.H1(children='Title of Dash App', style={'textAlign':'center'}),
    dcc.Dropdown(df.country.unique(), 'Canada', id='dropdown-selection'),
    dcc.Graph(id='graph-content')
])

@callback(
    Output('graph-content', 'figure'),
    Input('dropdown-selection', 'value')
)
def update_graph(value):
    dff = df[df.country==value]
    return px.line(dff, x='year', y='pop')

if __name__ == '__main__':
    app.run(debug=True)

```


```app.layout```で定義した```dropdown-selection```を変更するとcallback関数が呼び出され、グラフがupdateされるように書かれています。

上記ソースを```app.py```で保存し、```python app.py```で実行してコンソールに表示されたURLにアクセスすると、グラフが表示されます。
（※実行までの詳しい説明は今回は省きます。公式ページや他のWeb記事を参考にしてください。）
![初期画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1402694/6c8465dc-bc2c-ab64-7740-8e6b2703540a.png)
グラフエリアの上にあるドロップダウンで国名を選択すると、上述したcallbackが呼び出されてその国に応じたグラフが表示されるようになっています。

データと少しのpythonコードを用意するだけでここまで作れました。お手軽ですね。

## htmlを埋め込みたい要求現る
ここまででも見やすいグラフは書けているのですが、他に自前で用意したWebページとテーマを合わせることが必要になったりこのページ自体をオシャレにしたいという要求が出てきたりと、どうしてもhtml／cssが必要になる場合があります。pythonだけで書けるライブラリを採用したはずなのに、世の中ままならないものですね。
ちなみにDashにもcss相当の機能があったり、app.layoutの定義内で様々なデザインに対応したりできますが、やはり限界もあり効かない属性などがあるようです。
私も「取り込んでほしい」と渡されたhtmlを再現すべく、最初はhtml.Div内に色々と書き込んでみたのですが、WAI-ARIA属性が効かず詰みました。

話が少し逸れましたが、今回は↓こんな感じにタイトルとドロップダウンメニューをhtml化させ、Dashのグラフ領域と共存させたいという要求仕様が出てきたとします。
![画面計画.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1402694/e928e0e7-463e-1e43-d341-0de0ac081109.png)
用意したhtmlの画面はこちら。
![生html.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1402694/ff285d5d-a9fb-a13c-243a-68a1ab29fad8.png)
ドロップダウンメニュー用のhtmlとcssは↓こちらのサイトで作りました。ありがとうございます！

https://pote-chil.com/html-maker/selectbox

htmlとcssのソースまでここに載せると長くなってしまうので、ソースの中身を確認したい方は[こちらのGithub](https://github.com/ckw-1227/dash_html)のenbedding_htmlフォルダの中身を見てください。

それでは、html埋め込み作業に移りましょう。

### htmlとcssファイルを置く場所
まず大前提として、DashはFlaskベースのライブラリなので基本はFlaskのお作法に従うことになります。
htmlはtempletesフォルダ、cssはstatic/cssフォルダを作ってそこに入れます。フォルダ構成は以下のようになります。
```
app_root
┣ templates
┃  ┗ index.html
┣ static
┃  ┗ css
┃     ┗ dropdown.css
┗ app.py
```

### Dash(＿name＿)ではなくFlask(＿name＿)を呼ぶ
各ファイルが配置出来たら、app.pyを編集していきましょう。
今回のサンプルに限らず、多くのDashアプリでは最初に```app = Dash(__name__)```から書き始めていきますが、今回は```Flask(__name__)```を呼ぶようにします。
```python
from flask import Flask, render_template

flaskapp = Flask(__name__)
app = Dash(server=flaskapp, url_base_pathname='/app1/')
```
Flask（と後で使うのでrender_template）のインポートの追加もお忘れなく。
Flaskのサーバーをまず作って、Dashに噛ませる手順を取ります。
最後の行で指定している```url_base_pathname```は無意味なように見えますが、これを指定しないとhtmlとの共存が一切できないので省略しないでください。

### render_template()でhtmlファイル名を返す
flaskappを作ったことで、Flaskの```@route()```と```render_template()```が使えるようになります！
これらを使って、"/"のパス（ローカルで作っている場合は```http://127.0.0.1/```のURL）にアクセスしたときに自前で作ったhtmlファイルが表示されるように設定します。
```python
@flaskapp.route("/")
def index():

    return render_template(
        "index.html",
        dash_app=app.index()
    )
```
### htmlファイルからDashコンテンツを呼び出させる
次はhtmlファイルの方を編集します。
pythonファイルでrender_template()を使ってhtmlファイルを呼び出せるようになったので、今度はhtmlファイルとDashコンテンツを関連付けます。

方法はhtmlファイル中のDashコンテンツを入れたい場所に、jinjaの記法を使って```{{ dash_app|safe }}```と書きます。たったこれだけです。
今回はドロップダウンメニューの下にDashのグラフエリアを入れたいので、下記のようにしました。
```jinja
    <body>
        <div class="center"><h3>Dashとhtmlを共存させたい！</h3>
            <label class="selectbox-001">
                <select name="sources" id="sources" class="custom-select sources" placeholder="Choose Country">
                    <option value="Afghanistan">Afghanistan</option>
                    <option value="Albania">Albania</option>
                    <option value="Algeria">Algeria</option>
                    <option value="Angola">Angola</option>
                    <option value="Argentina">Argentina</option>
                    <option value="Armenia">Armenia</option>
                    <option value="Aruba">Aruba</option>
                    <option value="Australia">Australia</option>
                    <option value="Austria">Austria</option>
                </select>
            </label>
        </div>
        {{ dash_app|safe }}
    </body>
```
この記事では「Dashにhtmlを埋め込む」と表現していますが、ファイルの構成的には「htmlにDashを埋め込む」という言い方の方が正しいかもしれません。

### 一度起動してみる
ここまでで画面がどうなったのか、一度起動して確認してみましょう。
```python app.py```コマンドで起動すると下図のようにアクセス先URLが出ますが、ここで表示される```http://127.0.0.1:8050/app1/```ではなく、```http://127.0.0.1:8050/```でブラウザを立ち上げてください。
![アクセス先URL.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1402694/bbd50039-7dae-9de4-730b-403a43f397fa.png)
（```http://127.0.0.1:8050/app1/```の方にアクセスしてみると、Dashコンテンツのみが表示されます。）

```http://127.0.0.1:8050/```にアクセスすると、ちゃんとhtmlの内容とDashのグラフが共存しています！
![ひとまず起動してみる.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1402694/7e1c241a-8951-1116-a64a-abb21e7f21dc.png)

### Dashのいらないコンテンツを消す（隠す）
htmlとDashコンテンツの共存ができたので、Dashの方で定義されているタイトルとドロップダウンメニューが必要なくなりました。
できればきれいさっぱり消したいところですが、タイトルはともかくドロップダウンメニューの方は消すと少し問題があります。
ドロップダウンメニューのidである```dropdown-selection```がcallback関数のInput要素に指定されているため、layoutのドロップダウンメニュー定義のみ消してしまうとエラーが発生します。

callback関数も後で編集する必要がありますが、この記事では一旦、hiddenで隠すという手を使いましょう。
タイトルは削除し、ドロップダウンメニューを隠すとlayout定義のコードは下のようになります。
```python
app.layout = html.Div([
    html.Div([
        dcc.Dropdown(df.country.unique(), 'Canada', id='dropdown-selection')
    ], hidden=True),
    dcc.Graph(id='graph-content')
])
```

この状態でアプリケーションの再起動またはブラウザ更新をすると、
![ひとまず完成.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1402694/fac031af-8501-15da-a4d4-e4f8928cecf3.png)
作りたかったページを作成することができました！

### まだ完成ではない
画面の見た目を作ることはできましたが、まだ完成ではありません。
ドロップダウンメニューの国名を変えてみてもグラフの描画が変わらないからです。
callbackを全く変えていないのだから当然ですね。

ということで、html側のドロップダウンメニューとcallbackの関連付けをしたいのですが、ここで大きな問題が出てきます。
**html.Div内で定義した部品idでないとcallbackのInput/Outputに指定できない**のです。
自前で作ったドロップダウンメニューはidがhtmlファイル内で定義されているため、callback関数で直接扱うことができません。

html上のコンテンツとcallbackの関連付けに関しては、次の記事で手順を書いていこうと思います。
それでは次の記事へどうぞ～↓

https://qiita.com/ckw-1227/items/ba824cbc6a3fb3525bab