手っ取り早くソースを見たい方はGithubをどうぞ。
Dashに自作htmlを組み込みたい! ~html埋め込み編~で、Dashコンテンツとhtmlを共存させたページを作ることができました。
作成したページの画面イメージはこんな感じ。
上のタイトルとドロップダウンメニューがhtml、下のグラフエリアがDash製です。
さて、前の記事の最後に言及したように、この状態ではドロップダウンメニューを変更してもグラフと連動してくれません。
グラフとの連動をさせるためには、htmlで定義したドロップダウンメニューとcallback関数との関連付けが必要です。
Dashで定義したコンテンツであればcallback関数のInputに定義するだけで連動させることができるのですが、html.Div内で定義した部品idでないとcallbackのInput/Outputに指定できないためにhtml内の部品とcallback関数の関連付けには少々面倒な実装が必要になります。
callbackでInputできないならjsしかない
htmlで定義したコンテンツはそのままではDash内では使えません。
普段であればhtmlで定義したコンテンツのイベントや値を拾うためにはjavascriptを使います。
それなら、callback関数の中でjavascriptが使えれば何とかなるのでは?
…という一見無茶な願望を叶えてくれるライブラリがあります。
世の中探せばあるものですね。
pip install visdcc
でvisdccをインストールします。
それからapp.pyにimport visdcc
を加えることも忘れずに。
visdccにはjavascriptを実行するための機能のほかにもいくつか機能があるようですが、今回はそのうちのvisdcc.Run_js
を使います。
Githubにはいくつか実装例があるのですが、これから実装する内容はAdd_event_and_callbackを参考にしています。
選択した項目をDash側に連携するための部品を追加する
まずはapp.pyのlayout定義を編集して、選択項目をDash側に連携するための画面部品を追加しましょう。
追加するものは、
・Run_jsを実行するための宣言
・テキストボックス1つ
・ボタン1つ
です。
追加したあとのlayout定義は下のようになります。
app.layout = html.Div([
visdcc.Run_js(id='javascript'), # Run_js実行のための宣言
html.Div([
dcc.Dropdown(df.country.unique(), 'Canada', id='dropdown-selection')
], hidden=True),
dcc.Input(id='selected-text', value='Afghanistan'), # テキストボックス
html.Button(id='exec-button'), # ボタン
dcc.Graph(id='graph-content')
])
テキストボックスにはデフォルト値として、ドロップダウンのデフォルト値と同じ'Afghanistan'を入れておきます。
Dashを実行してみると、↓のようにテキストボックスと小さなボタンが追加されています。
テキストボックスとボタンでイベント連携
さて、追加したテキストボックスとボタンでどうするかというと、
ボタンはDash側で定義されているので、Inputに指定してイベント待ちをさせておく
↓
ドロップダウンが変更されたタイミングでボタンを押す
↓
callbackが稼働する
↓
Run_jsを使ってcallback内でjavascriptを実行し、htmlで定義されたドロップダウンの値を取得する
↓
取得したドロップダウンの値をテキストボックスに入力する
↓
テキストボックスの値をDash側で取得してグラフ描画に使用する
という順を追ってhtml側の入力値をDash側に渡します。
jsファイルを準備する
それでは実際に実装していきます。
まずは簡単なところから、ドロップダウンの値が変更されたらボタンを押すだけのjavascriptファイルを準備します。
前の記事でcssを準備したのと同じように、static/jsフォルダを作ってその中にjsファイルを作成します。
コードはこれだけ。runjs.js
というファイル名で保存しました。
$(function(){
$('#sources').change(function(){
$('#exec-button').click();
});
});
5行しかないのでhtmlファイルに書き込んでしまってもいいんですが、せっかくなので別ファイルで準備しました。
それからhtmlファイルにもjsファイルを呼び出すためのタグを追加しておきます。
<script src="https://code.jquery.com/jquery-3.7.1.slim.js" integrity="sha256-UgvvN8vBkgO0luPSUl2s8TIlOSYRoGFAX4jlCIm9Adc=" crossorigin="anonymous"></script>
<script src="static/js/runjs.js"></script>
callback関数を追加する
それではapp.pyファイルに戻って、今度はjavascriptを含めたcallback関数を追加しましょう。
以下の2つの関数を追加します。
@callback(
Output('javascript', 'run'),
Input('exec-button','n_clicks')
)
def event_from_button(x):
return '''
var country = $("#sources").val()
setProps({
'event': {'country':country}
})
'''
@callback(
Output('selected-text', 'value'),
Input('javascript', 'event')
)
def set_country(evt):
if evt is None:
return 'Afghanistan'
return evt['country']
先ほど準備したjsファイルの処理によってボタンが押されると、event_from_button関数が呼ばれます。
この関数の中ではOutputをjavascriptとして、文字列で書いたjavascriptをreturnします。
javascriptではドロップダウンで選択されている値を取得し、eventに入れる処理を行います。
ここで作ったeventはset_country関数のInputとなり、この関数内でOutput指定されているテキストボックスに値が入力される、という順で処理が進みます。
ここまでで一度app.pyを起動してみると、うまくいっていればドロップダウンで選択した値がテキストボックスに入力されるようになっているはずです。
ここまできたらあとひといき。
グラフ描画のための関数に値を渡す
最後はupdate_graph関数です。
Dash側で定義したdropdown-selection
がInputに指定してありましたが、これをテキストボックスのidであるselected-text
に変更します。
@callback(
Output('graph-content', 'figure'),
Input('selected-text', 'value')
)
def update_graph(value):
dff = df[df.country==value]
return px.line(dff, x='year', y='pop')
ここでもう一度起動してみましょう。ドロップダウンで選択した国名と連動して、グラフ描画がされるはずです!
不要な画面部品を消す・隠す
これで完成とするには不格好ですね。
テキストボックスとボタンは隠しておきたいです。
ついでに、layout定義に残っていたdcc.Dropdown
定義ももう必要ないので消してしまいましょう。
layout定義は↓こうなります。
app.layout = html.Div([
visdcc.Run_js(id='javascript'),
html.Div([
dcc.Input(id='selected-text', value='Afghanistan'),
html.Button(id='exec-button')
], hidden=True),
dcc.Graph(id='graph-content')
])
これでapp.pyも完成です。最終形のapp.py全体は以下のようになります。
from dash import Dash, html, dcc, callback, Output, Input
from flask import Flask, render_template
import plotly.express as px
import pandas as pd
import visdcc
df = pd.read_csv('https://raw.githubusercontent.com/plotly/datasets/master/gapminder_unfiltered.csv')
flaskapp = Flask(__name__)
app = Dash(server=flaskapp, url_base_pathname='/app1/')
@flaskapp.route("/")
def index():
return render_template(
"index.html",
dash_app=app.index()
)
app.layout = html.Div([
visdcc.Run_js(id='javascript'),
html.Div([
dcc.Input(id='selected-text', value='Afghanistan'),
html.Button(id='exec-button')
], hidden=True),
dcc.Graph(id='graph-content')
])
@callback(
Output('graph-content', 'figure'),
Input('selected-text', 'value')
)
def update_graph(value):
dff = df[df.country==value]
return px.line(dff, x='year', y='pop')
@callback(
Output('javascript', 'run'),
Input('exec-button','n_clicks')
)
def event_from_button(x):
return '''
var country = $("#sources").val()
setProps({
'event': {'country':country}
})
'''
@callback(
Output('selected-text', 'value'),
Input('javascript', 'event')
)
def set_country(evt):
if evt is None:
return 'Afghanistan'
return evt['country']
if __name__ == '__main__':
app.run(debug=True)
再掲載となりますが、完成したソース全体を見たい方はGithubへどうぞ。
完成
完成した画面イメージも貼っておきます。テキストボックスとボタンもちゃんと隠れています。
ですが、これは完全な完成とはちょっと言えません。
ドロップダウンの選択肢が少なすぎるんです。サンプルデータの描画対象国が100以上あり、htmlに全部書く前に挫折したからです。実はこのソースだと、Aから始まる国しか選べません笑
本当であれば、画面起動時にドロップダウンの値を動的に取得するべきなんですよね。その仕組みもRun_jsを使えば実現することができます。
ということで、Dashに自作htmlを組み込む手順をご紹介しました。
相当ニッチな記事であることは自覚しているのですが、いつかどこかで誰かの役に立てば幸いです。
それでは。