PythonのWEBアプリケーションフレームワークDashの公式ドキュメントのチュートリアルに沿ったまとめはこれが最後になります。
- 記事①: イントロ、Part.1〜Part.2
- 記事②: Part.3〜Part.5
- 記事③(本記事): Part.6、7、FAQ
他の記事と関連するplotlyの使い方については、下記を参照下さい。
Part.6 コールバック間でのデータの連携
- Dashの重要な原則の一つ: スコープの外側で変数を修正してはならない
- いかなるグローバル変数を修正することは安全でない
なぜ状態を共有するのか
- あるアプリケーションの中で、利用者はSQLでデータを取得したり、シミュレーションを走らせたり、データをダウンロードしたりといった、タスクを処理するために大きなデータに依存する、複数のコールバックを設定するかもしれない。
- しかし、それぞれのコールバックに同じような負荷の高いタスクを走らせるのではなく、1つのコールバックにタスクを走らせ、他のコールバックに結果を共有するという形をとることができる。
なぜグローバル変数がアプリケーションを壊すのか
- Dashは、複数のユーザーが同時にアプリケーションを利用し、それぞれ独立したセッションを持つような、マルチユーザ環境の中で機能するようにデザインされている。
- もし、アプリケーションがグローバル変数の値を修正する場合、あるユーザーのセッションが他のユーザのセッションに影響を与える値をその変数にセットできることになってしまう
- Dashは複数ユーザーに対応するように作られているため、コールバックは並行して実行することができる
- Dashでは複数のプログラムが並行して走っていても、メモリが共有されることはない ->これは1つのコールバック内でグローバル変数を修正しても、他のユーザーに影響を与えることはないということを意味する
# 信頼できないプログラム例->実行せず
df = pd.DataFrame({
'a': [1, 2, 3],
'b': [4, 1, 4],
'c': ['x', 'y', 'z'],
})
app.layout = html.Div([
dcc.Dropdown(
id='dropdown',
options=[{'label': i, 'value': i} for i in df['c'].unique()],
value='a'
),
html.Div(id='output'),
])
@app.callback(Output('output', 'children'),
[Input('dropdown', 'value')])
def update_output_1(value):
# ここで「df」は"この関数の外側にある"変数(グローバル変数)の一例。
# コールバックの中で(下のように)この変数を修正したり、新たな値を割り当てるのは安全ではない
global df = df[df['c'] == value] #(変数を修正している部分)安全でないため、これをしてはならない。
return len(df)
#上記の例を修正したプログラム
df = pd.DataFrame({
'a': [1, 2, 3],
'b': [4, 1, 4],
'c': ['x', 'y', 'z'],
})
app.layout = html.Div([
dcc.Dropdown(
id='dropdown',
options=[{'label': i, 'value': i} for i in df['c'].unique()],
value='a'
),
html.Div(id='output'),
])
@app.callback(Output('output', 'children'),
[Input('dropdown', 'value')])
def update_output_1(value):
# 安全のために、フィルターしたデータを新しい変数に割り当てている
filtered_df = df[df['c'] == value]
return len(filtered_df)
コールバック間でデータを共有する
- 複数のプロセスをまたいでデータを共有するためには、それぞれのプロセスがアクセスすることができる場所にデータを保存する必要がある
- データの保存場所候補は以下の3つ(それぞれ下記の3つのプログラム例に対応)
- ユーザーのブラウザのセッション
- ディスク上(ファイルや新たなデータベース)
- Redis(インメモリDB)のような共有メモリーの中
Example1. 隠されたdivタグを利用してブラウザでデータを保存
- ユーザのブラウザセッションでデータを保存する
- このフォーラムで説明されている方法を通して、Dashのフロントエンドの一部としてデータを保存するよう実装する
- 保存と送信により、データはJsonのような文字列に変換される必要がある
- このような方法でキャッシュされたデータは、ユーザーの現在のセッションにおいてのみ有効となる
- ユーザーが新しいブラウザを立ち上げるたびに、毎回Dashのコールバックはそのデータを計算する。そのデータはそのセッション内のコールバック間でだけキャッシュされ、送信される
- (メリット)キャッシュを利用する方法とは異なり、この方法はアプリケーションのメモリフットプリント(リソースの使用量)を増やさない
- (デメリット)この方法は、送信が必要なためネットワークコストが大きくなる可能性がある。コールバック間で10MBのデータを共有する場合、そのデータはそれぞれのコールバック間をネットワークをまたいで転送される
- ネットワークコストが高すぎる場合、先に全体のデータ計算してから転送する方法ができる。アプリケーションはおそらくは10MBのデータ全てを表示することはないが、そのデータの一部は表示する(サーバで全体を計算 -> ブラウザでは選択した部分だけ表示。ただし、全体のデータを持つため他の部分の表示にはその全体のデータの範囲内で素早く対応できる)
- 次のプログラム例は、どのようにして1つのコールバックで負荷の高いデータ処理を行い、Jsonでシリアライズ化し、それを他のコールバックのインプットとして提供するかを概略している
- シリアライズ化…ソフトウェア内部で扱っているデータをXMLやJsonなどの他のフォーマットに変換し、ネットワークで送受信できるようにすること
- このプログラム例では、Dashのスタンダードなコールバックを使用し、アプリの隠れdivタグ内でJson化されたデータを保存している
global_df = pd.read_csv('...') #データ
app.layout = html.Div([
dcc.Graph(id='graph'),
html.Table(id='table'),
dcc.Dropdown(id='dropdown'),
# 中間値を保存する、アプリア内の隠れdivタグ
html.Div(id='intermediate-value', style={'display': 'none'})
])
#①入力=Dropdown -> 出力=Div(中間保存場所)
@app.callback(Output('intermediate-value', 'children'),
[Input('dropdown', 'value')])
def clean_data(value):
# 負荷の高いデータ処理を処理し、Json化
cleaned_df = your_expensive_clean_or_compute_step(value)
・・・・
return cleaned_df.to_json(date_format='iso', orient='split')
#②入力=Div(中間保存場所にある①で計算したデータ) -> 出力=Graph
@app.callback(Output('graph', 'figure'),
[Input('intermediate-value', 'children')])
def update_graph(jsonified_cleaned_data):
#Jsonをpythonのデータ型に変換し再処理
・・・・
dff = pd.read_json(jsonified_cleaned_data, orient='split')
figure = create_figure(dff)
return figure
#③入力=Div(中間保存場所にある①で計算したデータ) -> 出力=Table
@app.callback(Output('table', 'children'),
[Input('intermediate-value', 'children')])
def update_table(jsonified_cleaned_data):
#Jsonをpythonのデータ型に変換し再処理
dff = pd.read_json(jsonified_cleaned_data, orient='split')
table = create_table(dff)
return table
<実行イメージ>
コールバック[入力->負荷の高い計算->保管場所にデータ保存]
-> データ保存場所=div(intermediate-value)
-> コールバック[保管場所からデータ取得->出力]
Example2. 先にデータ全体を計算する
- データが大きい場合、計算されたデータをネットワークで送るにはコストがかかりすぎる場合がある
- いくつかのケースでは、そのデータやJsonをシリアライズ化することもコストがかかる
- 多くのケースにおいて、アプリケーションは計算されたデータやフィルターがかけられたデータの一部や部分集合だけが表示される
- これらのケースでは、データ処理用コールバックでデータ全体を事前に計算し、残りのコールバックにこのデータを送ることができる
- 次のプログラム例は、フィルターがかけられたデータや集約されたデータをどのように複数のコールバックに転送するかを表している
#データの計算部
#入力=Dropdown -> データ計算 -> 出力=中間保管場所
@app.callback(
Output('intermediate-value', 'children'),
[Input('dropdown', 'value')])
def clean_data(value):
# an expensive query step
cleaned_df = your_expensive_clean_or_compute_step(value)
# a few filter steps that compute the data
# as it's needed in the future callbacks
df_1 = cleaned_df[cleaned_df['fruit'] == 'apples']
df_2 = cleaned_df[cleaned_df['fruit'] == 'oranges']
df_3 = cleaned_df[cleaned_df['fruit'] == 'figs']
datasets = {
'df_1': df_1.to_json(orient='split', date_format='iso'),
'df_2': df_2.to_json(orient='split', date_format='iso'),
'df_3': df_3.to_json(orient='split', date_format='iso'),
}
return json.dumps(datasets)
#入力=中間保管場所のデータ -> 出力=Graph(1)
@app.callback(
Output('graph', 'figure'),
[Input('intermediate-value', 'children')])
def update_graph_1(jsonified_cleaned_data):
datasets = json.loads(jsonified_cleaned_data)
dff = pd.read_json(datasets['df_1'], orient='split')
figure = create_figure_1(dff)
return figure
#入力=中間保管場所のデータ -> 出力=Graph(2)
@app.callback(
Output('graph', 'figure'),
[Input('intermediate-value', 'children')])
def update_graph_2(jsonified_cleaned_data):
datasets = json.loads(jsonified_cleaned_data)
dff = pd.read_json(datasets['df_2'], orient='split')
figure = create_figure_2(dff)
return figure
#入力=中間保管場所のデータ -> 出力=Graph(3)
@app.callback(
Output('graph', 'figure'),
[Input('intermediate-value', 'children')])
def update_graph_3(jsonified_cleaned_data):
datasets = json.loads(jsonified_cleaned_data)
dff = pd.read_json(datasets['df_3'], orient='split')
figure = create_figure_3(dff)
return figure
<実行イメージ>
コールバック[入力->負荷の高い計算(欲しいデータをあらかた計算)->保管場所にデータ保存]
-> データ保存場所(様々なデータを保存)
-> コールバック[保管場所から欲しいデータ取得->出力]
Example3. キャッシュとシグナリング
- グローバル変数を保存するために、Flask-cache経由でRedisを使う。このデータは、関数によってアクセスすることができ、この関数のアウトプットはキャッシュされ、インプット引数にによってキーとされる
- コストのかかる計算が完了した際に、他のコールバックにシグナルを送るために、隠しdivタグソリューションを利用する(上の例)
- Redisを利用する代わりに、ファイルシステムを利用することもできる。
- コストのかかる計算が1つのプロセスだけに収束するようになるため、この手法はとても優れている。この種の手法がなければ、1つの処理プロセスの代わりに4つの処理プロセスを必要とするように、それぞれのコールバックは、負荷の高い計算を並行して行う可能性がある
- このアプローチは、将来のセッションが事前に計算されたデータを利用できるという点でも優位性を持つ。これは、インプットが少ないアプリケーションではよく機能すると考えられる
- プログラム例は以下のようなもの
time.sleep(5)
は、利用して負担の大きいプロセスをシミュレートしたもの- アプリケーションがデータをロードしているとき、4つ全てのグラフを読み取るのに5秒かかっている
- 最初の計算は1プロセスのみをブロックする
- 一旦計算が終了すると、シグナルが送られ、グラフを読み取るために4つのコールバックが並行して実行される。これらのコールバックはそれぞれ、グロバールな保存場所(Redisやファイルシステム)からこれらのデータを引き出す
- アプリサーバー(
app.run_server
)の中で6つのプロセスが設定されており、そのため複数のプロセスが並行して実行される。 - あるデータが過去に既に選択されたことがある場合、ドロップダウンからその値を選んでも時間は5秒以下になる。これはそのデータがキャッシュから引き出されているため。
- 同様に、そのページのリロードや新しいタブでページを開いたとしても早くなる。これは初期の状態や最初の負荷の高い計算が既に完了しているため。
#app6-1.py
import os
import copy
import time
import datetime
import dash
import dash_core_components as dcc
import dash_html_components as html
import numpy as np
import pandas as pd
from dash.dependencies import Input, Output
from flask_caching import Cache
app = dash.Dash(__name__)
CACHE_CONFIG = {
'CACHE_TYPE': 'redis', #redisにセットしたくない場合は、"filesystem"を指定する
'CACHE_REDIS_URL': os.environ.get('REDIS_URL', 'localhost:6379')}
cache = Cache() #flask_caching
cache.init_app(app.server, config=CACHE_CONFIG)
N = 100
df = pd.DataFrame({
'category': (
(['apples'] * 5 * N) +
(['oranges'] * 10 * N) +
(['figs'] * 20 * N) +
(['pineapples'] * 15 * N)
)
})
df['x'] = np.random.randn(len(df['category']))
df['y'] = np.random.randn(len(df['category']))
app.layout = html.Div([
dcc.Dropdown(
id='dropdown',
options=[{'label': i, 'value': i} for i in df['category'].unique()],
value='apples'),
html.Div([
html.Div(dcc.Graph(id='graph-1'), className="six columns"),
html.Div(dcc.Graph(id='graph-2'), className="six columns"),],
className="row"),
html.Div([
html.Div(dcc.Graph(id='graph-3'), className="six columns"),
html.Div(dcc.Graph(id='graph-4'), className="six columns"),],
className="row"),
# 隠しDiv(シグナル)
html.Div(id='signal', style={'display': 'none'})
])
# グローバルスコープで負荷の高い計算を実行
# これらの計算結果はグローバルで利用可能なredisメモリーストアにキャッシュされる
# このredisメモリーストアはプロセスをまたぎ、かつ、いつでも利用可能
@cache.memoize()
def global_store(value):
# ここでは負荷の高い計算をシミュレートしている
print('Computing value with {}'.format(value))
time.sleep(5)
return df[df['category'] == value]
#figureオブジェクト生成用関数
def generate_figure(value, figure):
fig = copy.deepcopy(figure)
filtered_dataframe = global_store(value)
fig['data'][0]['x'] = filtered_dataframe['x']
fig['data'][0]['y'] = filtered_dataframe['y']
fig['layout'] = {'margin': {'l': 20, 'r': 10, 'b': 20, 't': 10}}
return fig
#入力=Dropdown -> 負荷の高い計算を実行 -> 出力=隠しDiv(signal)
@app.callback(Output('signal', 'children'),
[Input('dropdown', 'value')])
def compute_value(value):
#負荷の高い計算を実行し、完了した時にシグナルを送る
global_store(value)
return value
#入力=隠しDiv(シグナル、利用するデータを保持) -> 出力=Graph-1
@app.callback(Output('graph-1', 'figure'),
[Input('signal', 'children')])
def update_graph_1(value):
# generate_figureは、グローバルストアからデータを取得。
# グローバルストアの中のデータはcompute_valueコールバックで既に計算が完了し、グローバルなredisキャッシュの中で結果が保存されている
return generate_figure(value, {
'data': [{
'type': 'scatter',
'mode': 'markers',
'marker': {
'opacity': 0.5,
'size': 14,
'line': {'border': 'thin darkgrey solid'}}
}]
})
#入力=隠しDiv(シグナル、利用するデータを保持) -> 出力=Graph-2
@app.callback(Output('graph-2', 'figure'),
[Input('signal', 'children')])
def update_graph_2(value):
return generate_figure(value, {
'data': [{
'type': 'scatter',
'mode': 'lines',
'line': {'shape': 'spline', 'width': 0.5},
}]
})
#入力=隠しDiv(シグナル、利用するデータを保持) -> 出力=Graph-3
@app.callback(Output('graph-3', 'figure'),
[Input('signal', 'children')])
def update_graph_3(value):
return generate_figure(value, {
'data': [{
'type': 'histogram2d',}]
})
#入力=隠しDiv(シグナル、利用するデータを保持) -> 出力=Graph-4
@app.callback(Output('graph-4', 'figure'),
[Input('signal', 'children')])
def update_graph_4(value):
return generate_figure(value, {
'data': [{
'type': 'histogram2dcontour',}]
})
# DashのCSS
app.css.append_css({
"external_url": "https://codepen.io/chriddyp/pen/bWLwgP.css"})
# screen CSSのロード
app.css.append_css({
"external_url": "https://codepen.io/chriddyp/pen/brPBPO.css"})
if __name__ == '__main__':
app.run_server(debug=True, processes=6)
<実行イメージ>
コールバック[入力->負荷の高い計算->redisキャッシュにデータ保存・シグナル発出]
-> redisキャッシュ
-> コールバック[シグナル受容->redisキャッシュからデータ取得->出力]
Example4. サーバ上のユーザベースデータ
- Example3は、ファイルシステム上に計算データをキャッシュしたが、これらの計算データは誰でもアクセスができる
- あるケースでは、ユーザのセッションから独立してデータを保存したい時がある:あるユーザが引き出したデータは次のユーザが引き出したデータをアップデートすべきでない。これを行う方法は、Example1で行ったように隠しdivタグで保存することである
- これを行うもう1つの方法が、セッションIDを利用してファイルシステム上のキャッシュファイルにデータを保存し、セッションIDを使ってデータを参照する方法である。ネットワークをまたいでデータを転送する代わりにデータはサーバ上に保存されるため、この方法は一般的に隠しdivタグの方法よりも早くなる
- これはDashコミュニティのフォーラムのスレッドで議論されている
- このプログラム例の方法
- flask_cachingのファイルシステムキャッシュを利用してデータをキャッシュ。RedisのようなインメモリDBに保存する方法も利用できる。
- データをJsonとしてシリアル化
- pandasを利用している場合、Apache Arrowを利用したシリアル化を検討しても良い
- 想定される同時接続ユーザ数までユーザのセッションデータを保存する。これはデータがキャッシュで埋め尽くされるのを防ぐ
- ランダムな隠し文字列をアプリケーションのレイアウトの中に埋め込み、全てのページのロードで独自のレイアウトを提供することで一意のセッションIDを作る
- 注意点:データをクライアントに送るあらゆるプログラム例に関して、これらのセッションが必ずしも安全でなく、暗号化されていないということに注意する必要がある。これらのセッションIDは、セッションIDの固定化攻撃(session fixation)に脆弱な可能性がある
#app6-2.py
import dash
from dash.dependencies import Input, Output
import dash_core_components as dcc
import dash_html_components as html
import datetime
from flask_caching import Cache
import os
import pandas as pd
import time
import uuid
external_stylesheets = [
# Dash CSS
'https://codepen.io/chriddyp/pen/bWLwgP.css',
# screen CSSのロード
'https://codepen.io/chriddyp/pen/brPBPO.css']
app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
cache = Cache(app.server, config={
'CACHE_TYPE': 'redis',
#ファイルシステムキャッシュは、Herokuのような短命なファイルシステムを持ったシステムでは機能しない
'CACHE_TYPE': 'filesystem',
'CACHE_DIR': 'cache-directory',
# アプリ上で一度に対応できる数の最大数と等しくすべし
# より高い数字はファイルシステムやredisキャッシュにより多くのデータを保存する
'CACHE_THRESHOLD': 200
})
def get_dataframe(session_id):
@cache.memoize()
def query_and_serialize_data(session_id):
# 高価なデータ処理や一意のセッションでのデータ処理はここで行われる
# 時間に依存するデータを生成することで一意のセッションデータ処理をシミュレート
now = datetime.datetime.now()
time.sleep(5)
df = pd.DataFrame({
'time': [
str(now - datetime.timedelta(seconds=15)),
str(now - datetime.timedelta(seconds=10)),
str(now - datetime.timedelta(seconds=5)),
str(now)],
'values': ['a', 'b', 'a', 'c']})
return df.to_json()
return pd.read_json(query_and_serialize_data(session_id))
def serve_layout():
session_id = str(uuid.uuid4())
return html.Div([
html.Div(session_id, id='session-id', style={'display': 'none'}), #隠しDiv
html.Button('Get data', id='button'),
html.Div(id='output-1'),
html.Div(id='output-2')
])
app.layout = serve_layout
@app.callback(Output('output-1', 'children'),
[Input('button', 'n_clicks'),
Input('session-id', 'children')])
def display_value_1(value, session_id):
df = get_dataframe(session_id)
return html.Div([
'Output 1 - Button has been clicked {} times'.format(value),
html.Pre(df.to_csv()) ])
@app.callback(Output('output-2', 'children'),
[Input('button', 'n_clicks'),
Input('session-id', 'children')])
def display_value_2(value, session_id):
df = get_dataframe(session_id)
return html.Div([
'Output 2 - Button has been clicked {} times'.format(value),
html.Pre(df.to_csv())
])
if __name__ == '__main__':
app.run_server(debug=True)
- このプログラム例で気づくべき3つのこと
- データフレームのタイムスタンプは、データを引き出す時にアップデートされない。このデータはユーザセッションの一部としてキャッシュされる
- 当初、データを引き出す際は5秒かかるが、その後のクエリについては、キャッシュにデータが残されているため、すぐに処理される
- 2番目のデータは、1番目のデータとは異なるデータを映す。コールバック間で共有されたデータはここのユーザセッションとは異なるものとなる
Part7. FAQs and Gotchas
よく尋ねられる質問
Q. どのようにDashアプリの見栄えをカスタマイズすることができるか?
A. Dashアプリは、最新のスタンダードに準拠したwebアプリとしてブラウザで表現される。これは、標準のHTMLを使うようにアプリケーションのスタイルを設定するため、CSSを利用することができることを意味する。
全てのdash-html-components
は、style
属性を通してインラインのCSSスタイルをサポートしている。外付けのCSSスタイルシートも、コンポーネントの中のIDやクラスネームを指定することで、dash-html-components
やdash-core-components
のスタイルを設定するために利用することができる。dash-html-components
とdash-core-components
はclassName
属性を受け入れるが、これはHTML要素のclass
属性と同じである。
ユーザガイドのDash HTML Componentsセクションでは、インラインのスタイルとCSSスタイルシートで指定できるCSSクラスネームの両方を持ったdash-html-components
を利用する方法を説明している。DashガイドのAdding CSS & JS and Overriding the Page-Load Templateセクションは、スタイルシートとDashアプリをリンクする方法について説明している。
Q. どのようにしてDashアプリにJavaScriptを加えることができるか?
A. HTMLドキュメントにJavaScriptファイルを加えるように、Dashアプリに自身のスクリプトを加えることができる。Adding CSS & JS and Overriding the Page-Load Templateに記載。
Q. 複数ページを持ったDashアプリを作ることができるか?
A. できる!Dashは複数ページのアプリをサポートしている。Multi-Page Apps and URL Support
Q. どのようにして、複数のファイルにDashアプリを配置することができるか?
A. これを実現するためのストラテジーは、Dashユーザガイドの Multi-Page Apps and URL Supportに記載。
Q. どのようにして、どのInputが変わったか特定するか?
A. n_clicks属性(コンポーネントがクリックされた回数を調べる)に加えて、全てのdash-html-componentsはn_clicks_timestamp属性を持ち、そのコンポーネントが最後にクリックされた時間を記録している。これは、現在のコールバックを発火させるために、どのhtml.Buttonがクリックされたかを知る上で、便利な方法である。下記が一例である。
import dash
import dash_html_components as html
from dash.dependencies import Input, Output
app = dash.Dash(__name__)
#n_clicks_timestamp属性をそれぞれ指定
app.layout = html.Div([
html.Button('Button 1', id='btn-1', n_clicks_timestamp='0'),
html.Button('Button 2', id='btn-2', n_clicks_timestamp='0'),
html.Button('Button 3', id='btn-3', n_clicks_timestamp='0'),
html.Div(id='container')
])
@app.callback(Output('container', 'children'),
[Input('btn-1', 'n_clicks_timestamp'),
Input('btn-2', 'n_clicks_timestamp'),
Input('btn-3', 'n_clicks_timestamp')])
def display(btn1, btn2, btn3):
if int(btn1) > int(btn2) and int(btn1) > int(btn3):
msg = 'Button 1 was most recently clicked'
elif int(btn2) > int(btn1) and int(btn2) > int(btn3):
msg = 'Button 2 was most recently clicked'
elif int(btn3) > int(btn1) and int(btn3) > int(btn2):
msg = 'Button 3 was most recently clicked'
else:
msg = 'None of the buttons have been clicked yet'
return html.Div([
html.Div('btn1: {}'.format(btn1)),
html.Div('btn2: {}'.format(btn2)),
html.Div('btn3: {}'.format(btn3)),
html.Div(msg)
])
if __name__ == '__main__':
app.run_server(debug=True)
Q. Dashでは、Jinja2テンプレート(flaskの標準テンプレート)を使えるか?
A. Jinja2テンプレートは、HTMLページとしてクライアントに送られる前に、サーバ上に提示される(たいていはFlaskアプリサーバの中で)。一方で、DashアプリはReactを使って、クライアント上に提示される。これは、HTMLをブラウザで表示する上で、これら2つのアプローチが根本的に全く異なるものであり、直接的に結びつけることはできないということを意味する。ただし、FlaskアプリがあるURLエンドポイントを扱い、Dashアプリが特定のURLエンドポイントで利用されるというように、FlaskアプリとDashアプリを統合することはできる。
Q. DashでJQueryを使うことはできるか?
A. ほとんどの場合、使うことはできない。Dashはクライアントのブラウザにアプリケーション表示するために、Reactを利用する。Reactは、ページのレンダリングに仮想的なDOM(Document Object Model)を利用するという点でJQueryと根本的に異なっている。JQueryは、Reactの仮想的なDOMと通信することができないため、JQueryのページレイアウトを変えるためのいかなるDOM操作機能を使うことはできない。しかし、キーのタイプでページのリダイレクトをもたらすイベントリスナーを登録するような、JQueryのDOMにタッチしない一部の機能は使うことができる。
一般的に、アプリ上の仕様にクライアントサイドの振る舞いを加えようと模索しているのなら、custom Dash componentでその振る舞いをカプセル化するのがよい。
Q. もっと質問があるのだけれど、どこで尋ねれば良いか。
A. Dash Community forumsはDashのトピックを議論する人で溢れており、お互いの疑問を助け合い、Dashのクリエーションを共有している。リンクを辿って、議論に参加しよう!
Gotches
どのようにDashが機能するかについて半直感的でありうるいくつかの局面がある。これは特に、コールバックシステムがどのように機能しているかについて当てはまる。このセクションでは、より複雑なDashアプリを構築するときに遭遇する可能性がある、一般的なDash gotchasを概説する。もし、Dash Tutorialの残りを通読していてもなお、理解しきれていないのならば、ここは通読すべき良きセクションである。それでもまだ疑問が残るのであれば、Dash Community forumsは質問をぶつけるのに最適な場所である。
コールバックは、レイアウトで指定されたInput,State,Outputを必要とする
デフォルトで、Dashは妥当性の検証作業をコールバックに割り当てており、そこではコールバック引数のデータ型を検証したり、特定のInputやOutputコンポーネントが実際に特定の属性を持つかどうかの確認作業をチェックしたりするなどの検証が行われている。それゆえ、完全な妥当性検証のために、コールバック内のすべてのコンポーネントは、アプリの最初のレイアウトの中に存在していなければならず、もし存在していなければ、エラーが発生する。
しかし、ダイナミックなレイアウト変更を伴う、より複雑なDashアプリケーションの場合(複数ページのアプリケーションなど)、コールバックに存在する全てのコンポーネントが最初のレイアウトの中に含まれるわけではない。コールバックの妥当性検証を無効化することによって、この制約を取り去ることができる。
app.config.supress_callback_eceptions = True
<サマリー>
下記の妥当性検証がコールバックで行われる
app.layout = html.Div(
children=[....])
@app.callback(Output("id","property"), <--ここのコンポーネントがapp.layoutに存在しているか検証
[Input("id","property")]) <--ここのコンポーネントがapp.layoutに存在しているか検証
def some_function():
....
return some_return
※ 複数ページ使用の場合など、よりダイナミックな設計でcallbackで指定するコンポーネントが違うページのlayoutに指定される場合など、コールバックの妥当性検証が不要な場合はapp.config.supress_callback_eceptions = True
を指定する
コールバックはページ上で表示される、全てのInputs
,States
,Output
を必要とする
ダイナミックレイアウトをサポートするためにコールバックの妥当性検証を無効化した場合、コールバック内のコンポーネントがレイアウト内に見つからない状況の場合でも自動的にアラートが発されることはない。コールバックで登録されたコンポーネントがレイアウトに存在しない、このような状況の場合、コールバックは発火し損なう。例えば、現在のページレイアウトの中で存在する特定のInputs
の一部を使って、コールバックを定義する場合、そのコールバックは単純に、全く作動しないことになる。
コールバックは1つのOutputコンポーネント/プロパティペアだけターゲットにすることができる¶
現在、コールバックは、一つのOutputだけを持つことができ、これは一つのコンポーネント/プロパティペア(component_id=my-graph/component_property=figure)をターゲットにしている。4つのGraphコンポーネントが特定のユーザインプットに基づいて、アップデートされるようにしたい場合、それぞれが個々のGraphをターゲットとする、それぞれ独立した4つのコールバックを作るか、そのコールバックに、アップデートされた4つのグラフをもつhtml.Divコンテナを返させるかする必要がある。
この制限を取り除くプランがあり、Github Issueでこのプランの進捗状況を追っていくことができる。
<サマリー>
@app.callback(Output(), <-アウトプットは1つだけ
[Input(),...]) <-インプットはリストで複数与えることができる
def some_functions():
....
return some_return
・特定のインプットがあった場合に複数(ここでは4つ)のアウトプットを生み出したい
解決策①: それぞれのアウトプットに対応した複数のコールバックを定義する
#コールバック(1)
@app.callback(Output(component_id='graph-1',...), <--アウトプット先が異なる
[Input(component_id='input',...)])
def some_function():
...
#コールバック(2)
@app.callback(Output(component_id='graph-2',...), <--アウトプット先が異なる
[Input(component_id='input',...)])
def some_function():
...
#コールバック(3)
@app.callback(Output(component_id='graph-3',...), <--アウトプット先が異なる
[Input(component_id='input',...)])
def some_function():
...
#コールバック(4)
@app.callback(Output(component_id='graph-4',...), <--アウトプット先が異なる
[Input(component_id='input',...)])
def some_function():
...
解決策②:一つのコールバックそれぞれのグラフを収めた4つのhtml.Divを返させる
@app.layout=html.Div(
children=[html.div(id='input_a',value=...),
html.div(id='graphs')])
@app.callback(Output(component_id='graphs',component_property='children'),
[Input(component_id='input_a',),...])
def some_function():
....
return html.Div(id='graph-1',...), html.Div(id='graph-2',...), ... #html.Div4つ
※コールバックのリターンがlayoutの'graphs'に順に格納されるイメージ?このコードが機能するか自信無し...
全てのコールバックはサーバがスタートする前に定義されなければならない
全てのコールバックはDashアプリサーバが起動し始める前に定義されなければならず、これはapp.run_server(debug=True)
の前のことを指す。これは、コールバックの処理中にダイナミックにレイアウトの断片を変更することができる一方で、コールバックの処理過程でユーザインプットに反応して、コールバックをダイナミックに定義づけることはできないということを意味する。インプットをコントロールする様々なセットを含むために、コールバックがレイアウトを変えるような、ダイナミックなインターフェイスを持つ場合、これらの新しいコントロールを満たすために必要とされるコールバックを前もって定義づけておかなければならない。
例えば、一般的なシナリオとして、あるダッシュボードを、異なるコントロールセットも持ったり(ユーザインプットに依存するような数字や種類など)、基礎となるデータを生成するための異なるロジックを持津ような、論理的に明確に区別される他のダッシュボードと取り替えるために現在のレイアウトをアップデートするDropdown
コンポーネントが当てはまる。実用的な構成は、異なるコールバックを持つこれらのダッシュボードのためである。このシナリオでは、これらのコールバックのそれぞれは、アプリケーションが起動される前に定義されなければならない。
一般論として、Dashアプリの特徴が、ユーザインプットによってInputs
やStates
の数が決められるようなものである場合、潜在的にユーザが選択する可能性があるコールバックの発生順序を前もって、定義しておく必要がある。コールバックデコレーターを用いて、これがどのようにプログラム的に実装されるかの例として、Dash Community forum post。
レイアウトのすべてのDashコアコンポーネントは、コールバックに登録されなければならない
Dashコアコンポーネントがレイアウトの中に存在するが、コールバックに登録されていない場合(Input
,State
,Output
)、何らかのコールバックがそのページをアップデートするとき、ユーザによるその値のいかなる変更もオリジナルバリューによってリセットされてしまう。
これはよく知られたイシューであり、Github Issueでこのステータスを追うことができる。
最後に
以上で公式ドキュメントのチュートリアルになります。Dashはまだまだ知名度が低いようですが、スタイリッシュなグラフを描写できるので、データサイエンスなどの絡みで今後利用が増えていくかもしれません。また、基盤がflaskとなっており、flaskをベースにグラフページはDashを利用するという使い方もできるようです。
今現在作成中のWEBアプリに使ってみようと思います。読んで頂いた方の参考になれば幸いです。