在上一章中,我們了解到dash.layout描述了應用程序的外觀,是一個組件的層次結構樹。dash_html_components庫為所有html標記提供類,關鍵字參數來描述html屬性,如樣式、類名和id。dash_core_components庫生成更高級別的組件,如控件和圖形。
本章介紹如何使用回調函數(callback functions)制作Dash應用程序:當輸入組件的屬性發生更改時,Dash會自動調用這些函數,以更新另一個組件(輸出)中的某些屬性。
為了獲得最佳的用戶交互和圖表加載性能,生產DASH應用程序應該考慮Job Queue、HPC、DATASADER和DASH的水平縮放能力。
讓我們從一個交互式Dash應用程序的簡單示例開始。
一個簡單的Dash應用
運行以下代碼:
import dash
from dash import dcc
from dash import html
from dash.dependencies import Input, Output
app = dash.Dash(__name__)
app.layout = html.Div([
html.H6("Change the value in the text box to see callbacks in action!"),
html.Div([
"Input: ",
dcc.Input(id='my-input', value='initial value', type='text')
]),
html.Br(),
html.Div(id='my-output'),
])
@app.callback(
Output(component_id='my-output', component_property='children'),
Input(component_id='my-input', component_property='value')
)
# 我們下面定義的這個函數會自動被回填函數調用
def update_output_div(input_value):
return 'Output: {}'.format(input_value)
if __name__ == '__main__':
app.run_server(debug=True)
運行結果如下:
下面來解釋代碼的意思:
- 我們的app中的輸入和輸出被描述為
@app.callback
調試器中的參數- 在本例中,我們的
dcc.Input(id= ,value= ,type= )
中,id='my-input'
相當於這個輸入框的名字是my-input
,value='initial value'
指的是輸入框中的默認值是initial value
,這也是為什么我們在回調函數中寫Input(component_id='my-input', component_property='value')
的原因,component_id='my-input'
表示回調函數中的輸入就是前面dcc.Input
中的值。對於dcc.Output
也是同理。- 每當輸入屬性發生更改時,回調裝飾器包裝的函數將被自動調用。Dash為這個回調函數提供輸入屬性的新值作為其參數,Dash用函數返回的任何內容更新輸出組件的屬性。
component_id
和component_property
關鍵字是可選的(每個對象只有兩個參數)。為了清晰起見,本示例中包含了這些內容,但為了簡潔易讀,本文檔的其余部分將省略這些內容。- 千萬別把
dash.dependencies.Input
和dcc.Input
搞混了。前者僅在這些回調定義中使用,后者是一個實際組件。- 請注意,我們沒有在布局中為
my_output
組件的children
屬性設置值。Dash應用程序啟動時,它會自動使用輸入組件的初始值調用所有回調,以填充輸出組件的初始狀態。在本例中,如果將div組件指定為html.Div(id='my-output',children='Hello world')
,它將在應用程序啟動時被覆蓋,也就是說寫了等於白寫,還不如不寫。
這有點像用Microsoft Excel編程:每當一個單元格(輸入)發生變化時,依賴該單元格(輸出)的所有單元格都會自動更新。因為輸出會自動對輸入的變化做出反應,所以這被稱為“反應式編程”(Reactice Programming)。
還記得每個組件是如何完全通過一組關鍵字參數來描述的嗎?我們在Python中設置的這些參數成為組件的屬性,這些屬性現在很重要。借助Dash的交互性,我們可以使用回調動態更新這些屬性中的任何一個。我們通常會更新HTML組件的children
屬性以顯示新文本(記住,children
組件負責組件的內容)或dcc.Graph
的figure
屬性以顯示新數據。我們還可以更新組件的style
,甚至更新dcc.Dropdown
中的可用選項!
讓我們來看看dcc.Slider
如何更新dcc.Graph
的吧?
帶數字和滑塊的Dash應用程序布局
import dash
from dash import dcc
from dash import html
import plotly.express as px
import pandas as pd
from dash.dependencies import Input, Output
df = pd.read_csv('https://raw.githubusercontent.com/plotly/datasets/master/gapminderDataFiveYear.csv')
app = dash.Dash(__name__)
app.layout = html.Div([
dcc.Graph(id='graph-with-slider'),
dcc.Slider(
id='year-slider',
min=df['year'].min(),
max=df['year'].max(),
value=df['year'].min(),
marks={str(year): str(year) for year in df['year'].unique()},
step=None
)
])
@app.callback(
Output('graph-with-slider', 'figure'),
Input('year-slider', 'value'))
def update_figure(selected_year):
filtered_df = df[df.year == selected_year]
fig = px.scatter(filtered_df, x="gdpPercap", y="lifeExp",
size="pop", color="continent", hover_name="country",
log_x=True, size_max=55)
fig.update_layout(transition_duration=500)
return fig
if __name__ == '__main__':
app.run_server(debug=True)
注:代碼中的csv文件需要FQ才能下載
運行結果如下:
在本例中,dcc.Slider
的value
屬性是這個app的輸入,dcc.Grahp
的figure
屬性是這個app的輸出。無論何時dcc.Slider
的value
被改變,Dash都會調用update_figure
這個回調函數來實時更新圖像。這個函數會根據滑塊的值自動過濾DataFrame中的數據,然后將處理過的數據返回給figure
,畫出新的圖像。
這里有幾點值得注意的地方:
- 我們再此代碼的最開頭用Pandas這個庫來加載數據:
df = pd.read_csv('''')
。這個dataframe(df)是一個全局變量,能夠被回調函數調用。- 將數據加載到內存可能會很昂貴。通過在應用程序開始時而不是在回調函數中加載查詢數據,我們可以確保此操作只在應用程序服務器啟動時執行一次。當用戶訪問應用程序或與應用程序交互時,該數據(df)已經存在於內存中。如果可能的話,昂貴的初始化(如下載或查詢數據)應該在應用程序的全局范圍內完成,而不是在回調函數中完成。
- 回調不會修改原始數據,它只通過使用pandas進行過濾來創建dataframe的副本。這一點很重要:回調絕不能修改其范圍之外的變量。如果回調修改了全局狀態,那么一個用戶的會話可能會影響下一個用戶的會話,並且當應用部署在多個進程或線程上時,這些修改不會在會話之間共享。
- 我們正在使用
layout.transition
來像動畫一樣對數據進行隨時間而做平滑的改變。
具有多個輸入的Dash應用程序
在Dash中,任何輸出都可以有多個輸入組件。下面是一個簡單的示例,它將五個輸入(兩個dcc.Dropdown
組件、兩個dcc.radiotems
組件和一個dcc.Slider
組件的value
屬性)綁定到一個輸出組件(dcc.Graph
組件的figure
屬性)。注意這個應用程序是如何運行的。callback
如何在輸出之后列出所有五個輸入項。
import dash
from dash import dcc
from dash import html
from dash.dependencies import Input, Output
import plotly.express as px
import pandas as pd
app = dash.Dash(__name__)
df = pd.read_csv('https://plotly.github.io/datasets/country_indicators.csv')
available_indicators = df['Indicator Name'].unique()
app.layout = html.Div([
html.Div([
html.Div([
dcc.Dropdown(
id='xaxis-column',
options=[{'label': i, 'value': i} for i in available_indicators],
value='Fertility rate, total (births per woman)'
),
dcc.RadioItems(
id='xaxis-type',
options=[{'label': i, 'value': i} for i in ['Linear', 'Log']],
value='Linear',
labelStyle={'display': 'inline-block'}
)
], style={'width': '48%', 'display': 'inline-block'}),
html.Div([
dcc.Dropdown(
id='yaxis-column',
options=[{'label': i, 'value': i} for i in available_indicators],
value='Life expectancy at birth, total (years)'
),
dcc.RadioItems(
id='yaxis-type',
options=[{'label': i, 'value': i} for i in ['Linear', 'Log']],
value='Linear',
labelStyle={'display': 'inline-block'}
)
], style={'width': '48%', 'float': 'right', 'display': 'inline-block'})
]),
dcc.Graph(id='indicator-graphic'),
dcc.Slider(
id='year--slider',
min=df['Year'].min(),
max=df['Year'].max(),
value=df['Year'].max(),
marks={str(year): str(year) for year in df['Year'].unique()},
step=None
)
])
@app.callback(
Output('indicator-graphic', 'figure'),
Input('xaxis-column', 'value'),
Input('yaxis-column', 'value'),
Input('xaxis-type', 'value'),
Input('yaxis-type', 'value'),
Input('year--slider', 'value'))
def update_graph(xaxis_column_name, yaxis_column_name,
xaxis_type, yaxis_type,
year_value):
dff = df[df['Year'] == year_value]
fig = px.scatter(x=dff[dff['Indicator Name'] == xaxis_column_name]['Value'],
y=dff[dff['Indicator Name'] == yaxis_column_name]['Value'],
hover_name=dff[dff['Indicator Name'] == yaxis_column_name]['Country Name'])
fig.update_layout(margin={'l': 40, 'b': 40, 't': 10, 'r': 0}, hovermode='closest')
fig.update_xaxes(title=xaxis_column_name,
type='linear' if xaxis_type == 'Linear' else 'log')
fig.update_yaxes(title=yaxis_column_name,
type='linear' if yaxis_type == 'Linear' else 'log')
return fig
if __name__ == '__main__':
app.run_server(debug=True)
輸出如下:
在這個例子中,只要dcc.Dropdown
,dcc.Slider
或dcc.RadioItems
中的任意一個改變,圖像就會發生對應的變化!
回調的輸入參數是每個“輸入”屬性的當前值,按指定的順序排列。
即使一次只更改一個輸入(即,用戶在給定時刻只能更改一個下拉列表的值),Dash也會收集所有指定輸入屬性的當前狀態,並將它們傳遞給回調函數。這些回調函數始終保證接收應用程序的更新狀態。
讓我們擴展我們的示例以包括多個輸出。
具有多個輸出的Dash應用程序
到目前為止,我們編寫的所有回調只更新一個輸出屬性。我們還可以一次更新多個輸出:列出所有要在應用程序中更新的屬性。回調,並從回調中返回多項。如果兩個輸出依賴於相同的計算密集型中間結果(意思就是如果兩個輸出的計算相同且很復雜,那么這么做就能減少計算量,節省時間),例如緩慢的數據庫查詢,這一點尤其有用。
復制並運行下面代碼:
import dash
from dash import dcc
from dash import html
from dash.dependencies import Input, Output
external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
app.layout = html.Div([
dcc.Input(
id='num-multi',
type='number',
value=5
),
html.Table([
html.Tr([html.Td(['x', html.Sup(2)]), html.Td(id='square')]),
html.Tr([html.Td(['x', html.Sup(3)]), html.Td(id='cube')]),
html.Tr([html.Td([2, html.Sup('x')]), html.Td(id='twos')]),
html.Tr([html.Td([3, html.Sup('x')]), html.Td(id='threes')]),
html.Tr([html.Td(['x', html.Sup('x')]), html.Td(id='x^x')]),
]),
])
@app.callback(
Output('square', 'children'),
Output('cube', 'children'),
Output('twos', 'children'),
Output('threes', 'children'),
Output('x^x', 'children'),
Input('num-multi', 'value'))
def callback_a(x):
return x**2, x**3, 2**x, 3**x, x**x
if __name__ == '__main__':
app.run_server(debug=True)
首先讓我來對上述代碼做一個簡單的說明:html.Tabel()
是創建一個表格,里面的值是一個列表,列表中元素是html.Tr()
,每個html.Tr()
代表一行,而html.Td()
代表的是一個格子。
代碼的運行結果如下:
警告:合並輸出並不總是一個好主意:
- 如果輸出依賴於相同輸入的部分(而不是全部),那么將它們分開可以避免不必要的更新。
- 如果輸出具有相同的輸入,但它們使用這些輸入執行非常不同的計算,保持回調分離可以允許它們並行運行。
帶鏈接回調的Dash應用程序
您還可以將輸出和輸入鏈接在一起:一個回調函數的輸出可以作為另一個回調函數的輸入。
此模式可用於創建動態UI,例如,一個輸入組件更新另一個輸入組件的可用選項。下面是一個簡單的例子。
復制並運行下面的代碼:
# -*- coding: utf-8 -*-
import dash
from dash import dcc
from dash import html
from dash.dependencies import Input, Output
external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
all_options = {
'America': ['New York City', 'San Francisco', 'Cincinnati'],
'Canada': [u'Montréal', 'Toronto', 'Ottawa']
}
app.layout = html.Div([
dcc.RadioItems(
id='countries-radio',
options=[{'label': k, 'value': k} for k in all_options.keys()],
value='America'
),
html.Hr(),
dcc.RadioItems(id='cities-radio'),
html.Hr(),
html.Div(id='display-selected-values')
])
#選擇不同的國家后,下面的表會顯示對應國家的城市
@app.callback(
Output('cities-radio', 'options'),
Input('countries-radio', 'value'))
def set_cities_options(selected_country):
return [{'label': i, 'value': i} for i in all_options[selected_country]]
#當選擇新的國家后,需要設置一個默認城市,所以統一設置為第一個城市
@app.callback(
Output('cities-radio', 'value'),
Input('cities-radio', 'options'))
def set_cities_value(available_options):
return available_options[0]['value']
#依據國家和城市輸出一句話
@app.callback(
Output('display-selected-values', 'children'),
Input('countries-radio', 'value'),
Input('cities-radio', 'value'))
def set_display_children(selected_country, selected_city):
return u'{} is a city in {}'.format(
selected_city, selected_country,
)
if __name__ == '__main__':
app.run_server(debug=True)
基於第一個dcc.RadioItems
的選項,第一個回調更新第二個dcc.RadioItems
中的可用選項。
當options
屬性更改時,第二個回調將設置初始值:它將其設置為該選項數組中的第一個值。
最后一個回調顯示每個組件的選定值。如果更改國家/地區dcc.RadioItems
的值,Dash將等到城市組件的值更新后再調用最終回調。這可以防止您的回調被稱為不一致的狀態,如“American”和“Montreal”。
帶狀態的Dash應用程序
在某些情況下,應用程序中可能會有一個類似於“表單”的模式。在這種情況下,你可能希望程序在用戶徹底輸入完信息后再進行運算,而不是用戶一改變其中的部分信息,程序就馬上更新。
將回調直接附加到輸入值可以如下所示:
# -*- coding: utf-8 -*-
import dash
from dash import dcc
from dash import html
from dash.dependencies import Input, Output
external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"]
app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
app.layout = html.Div([
dcc.Input(id="input-1", type="text", value="Montréal"),
dcc.Input(id="input-2", type="text", value="Canada"),
html.Div(id="number-output"),
])
@app.callback(
Output("number-output", "children"),
Input("input-1", "value"),
Input("input-2", "value"),
)
def update_output(input1, input2):
return u'Input 1 is "{}" and Input 2 is "{}"'.format(input1, input2)
if __name__ == "__main__":
app.run_server(debug=True)
在本例中,只要輸入描述的任何屬性發生更改,就會觸發回調函數。State
解決了個問題,它能夠允許你輸入額外值而不調動回調函數!這里有一個和前面相似的程序,但是兩個dcc.Input
組件擁有State
和新的按鈕。說人話就是在輸入的后面加了一個啟動回調函數的按鈕。
復制並運行下面的代碼:
# -*- coding: utf-8 -*-
import dash
from dash import dcc
from dash import html
from dash.dependencies import Input, Output, State
import dash_design_kit as ddk
external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
app.layout = html.Div([
dcc.Input(id='input-1-state', type='text', value='Montréal'),
dcc.Input(id='input-2-state', type='text', value='Canada'),
html.Button(id='submit-button-state', n_clicks=0, children='Submit'),
html.Div(id='output-state')
])
@app.callback(Output('output-state', 'children'),
Input('submit-button-state', 'n_clicks'),
State('input-1-state', 'value'),
State('input-2-state', 'value'))
def update_output(n_clicks, input1, input2):
return u'''
The Button has been pressed {} times,
Input 1 is "{}",
and Input 2 is "{}"
'''.format(n_clicks, input1, input2)
if __name__ == '__main__':
app.run_server(debug=True)
代碼的運行結果如下:
你可以嘗試一下改變輸入框里的值,你會發現只有點擊SUBMIT時下面的值才會改變,這就是State
的效果。
在本例中,更改dcc.Input()
中的文本不會觸發回調,但單擊按鈕會觸發回調。即使它們不會觸發回調函數本身,但是dcc.Input
當前的值仍然會傳遞到回調函數中。
注意,我們通過監聽html
的n_clicks
屬性來判斷是否觸發回調。n_clicks
是一個屬性,它在每次單擊組件時都會遞增。dash_html_components
庫中的每個組件都可以使用它,但最好和按鈕配合使用。
本章總結
我們已經在Dash中介紹了回調的基本原理。Dash應用程序基於一組簡單但功能強大的原則構建:用戶界面可通過反應式回調進行定制。組件的每個屬性/屬性都可以作為回調的輸出進行修改,而屬性的子集(例如dcc.Dropdown
組件的value
屬性)可以由用戶通過與頁面交互進行編輯。
下一章我們將學習如何將回調函數應用到畫圖中。