本章描述如何使用回调函数来制作你的Dash应用程序:当输入组件的属性发生变化时,Dash会自动调用这些Python函数。
1.简单栗子
import dash
import dash_core_components as dcc
import dash_html_components as 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([
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.callback
装饰器通过声明,描述应用程序界面的“输入”与“输出”项;- 我们的应用程序的
**Input**
和**output**
只是特定组件的属性。在本例中,我们**Input**
的**value**
值是**ID**
为 “**my-input**
“。我们的**Output**
是**ID**
为“**my-output**
”的组件的“**children**
”属性。 - 每当输入属性发生变化时,回调装饰器包装的函数将被自动调用。Dash将输入属性的新值作为输入参数提供给函数,Dash使用函数返回的内容更新输出组件的属性。
- 不要混淆
dash.dependencies.Input
和dash_core_components
对象。前者只是在这些回调中使用,而后者是一个实际的组件。 - The
component_id
andcomponent_property
可以被省略 - Dash应用程序启动时,会自动使用输入组件的初始值,调用所有的回调函数,以填充输出组件的初始值。所以,不要在layout中设置 my-div组件的children特性,本例中,如果指定了
html.Div(id='my-div', children='Hello world')
的内容,应用启动时会被覆盖。这种方式类似于Microsoft Excel编程:当单元格的内容发生变化时,依赖于该单元格的所有单元格的内容,都将自动更新。这称为 “反应式编程” (Reactive Programming) 。
@callback的详细用法:
- 通过编写这个装饰器,我们告诉Dash在“input”组件(文本框)的值发生变化时为我们调用这个函数,以便更新页面上“output”组件的子组件(HTML div)
- 可以使用任何的函数名称跟在@callback之后,但必须在回调函数中使用与定义中相同的名称(就和python语法一样)。约定是用名称来描述回调输出。
- 参数是有位置的。第一个(最底下那个)是
Input
,后面跟任何state
,顺序与装饰器中相同。- 当把Dash组件作为
@appback
的输入或输出时,你必须使用与app.layout
中Dash组件相同的id
。回调装饰。callback
后面紧跟着函数,不能有空行。- If you’re curious about what the decorator syntax means under the hood, you can read this StackOverflow answer and learn more about decorators by reading PEP 318 — Decorators for Functions and Methods.
2.滑块
import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output
import plotly.express as px
import pandas as pd
df = pd.read_csv('https://raw.githubusercontent.com/plotly/datasets/master/gapminderDataFiveYear.csv')
external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
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)
- 本例中,app的输入是
**Slider**
的属性value
,app的输出是**Graph**
的属性figure
。当Slider
的value
变化时,Dash用新值调用回调函数update_figure
,该函数使用此新值过滤数据框,构造figure
对象,并将其返回到Dash应用程序中,作为输出; - 使用关键字参数进行组件描述,很重要。通过Dash交互性,使用回调函数,可以动态地更新这些特性。如:更新组件的
**children**
属性从而更新文本内容、更新**dcc.Graph**
组件的**figure**
属性从而更新数据、更新组件的**style**
属性从而更新画布样式、更新**dcc.Dropdown**
组件的**options**
从而更新下拉菜单; - 将数据加载至内存并进行计算的代价很高,所以尽量在应用的全局范围内下载或查询数据,避免在回调函数里进行这类操作,确保用户访问或与应用交互时,数据(df)已经载入至内存。本例中df获取的数据是全局的,可以被回调函数读取;
- 回调函数不会修改原始数据,只是通过
**Pandas**
的过滤器来筛选数据,并创建DataFrame
的副本。这点非常重要:不要在回调函数范围之外更改变量。如果在全局状态下调整回调函数,某一用户的会话就可能影响下一用户的会话,特别是应用部署在多进程或多线程的环境时,这些修改可能会导致跨会话数据分享出现问题;
3.多端输入
在Dash中,任何“Input
”都可以有多个“Output
”组件。下面是一个简单的例子,它将五个输入(两个Dropdown
组件的value
属性、两个RadioItems
组件和一个Slider
组件)绑定到一个输出组件(Graph
组件的figure
属性)。回调函数的第二个参数,列表中列举了所有的五个输入项dash.dependencies.Input
。
import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output
import plotly.express as px
import pandas as pd
external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
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)
- 在本例中,每当
Dropdown
、Slider
或RadioItems
组件的value
属性更改时,就会调用update_graph
函数。 update_graph
函数的输入参数是每个输入属性的新值或当前值,按指定的顺序排列。- 即使一次只更改一个输入(用户在给定时刻只能更改单个下拉列表的值),Dash也会收集所有指定输入属性的当前状态,并将它们传递到函数中。你的回调函数总是保证会被传递给应用程序的代表状态。
4.多端输出
import dash
import dash_core_components as dcc
import dash_html_components as 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)
- 一个Dash回调函数只能更新一个输出属性。要想实现多重输出,需要编写多个函数;
- 具体方法:将需要更新的所有属性,作为列表添加到装饰器中,并从回调中返回多个输出项。如果两个输出依赖于相同的计算密集型中间结果,例如慢速数据库查询,推荐使用该方法;
- 组合输出并不总是一个好主意:1)如果输出依赖于某些但不是所有相同的输入,则将它们分开可以避免不必要的更新;2)如果它们具有相同的输入,但使用这些输入进行独立计算,则将回调分开,可以实现并行运行它们;
5.Chained Callbacks
一个回调函数的输出是另一个回调函数的输入。
# -*- coding: utf-8 -*-
import dash
import dash_core_components as dcc
import dash_html_components as 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)
- 链式回调:将输出和输入链接在一起,即一个回调函数的输出是另一个回调函数的输入;
- 此模式用于创建动态UI,其中一个输入组件更新下一个输入组件的可用选项;
- 第二个单选按钮RadioItems的选项,基于第一个回调函数传递的单选按钮RadioItems中选择的值;
- 第二个回调函数设置了options特性改变时的初始值:将自身设置为options数组中的第一个值;
- 最后的回调函数,显示了每个组件中的可选内容。如果更改了城市单选按钮RadioItems组件的value属性,则Dash将等待,直到value更新状态组件后,再调用最后的回调函数。
6.状态回调
# -*- coding: utf-8 -*-
import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output, State
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
才会更新返回值。dcc.Input
的值依然会传入到回调参数中,但是仅仅是作为State
传入,当SUBMIT
的控件触发后,才会把State
的value传入到函数中。
7.小结
- Dash应用是基于下述简单但强大的原则进行构建的:通过响应式与函数式的Python回调函数,自定义声明式的UI;
- 声明式组件中的每个元素属性,都可以通过回调函数和属性子集进行更新,比如dcc.Dropdown的value特性,这样用户就可以在交互界面中进行编辑。
注释参考:https://www.jianshu.com/p/cb0dc98e00bc
官方文档:https://dash.plotly.com/basic-callbacks