在上一章中,我们了解到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
属性)可以由用户通过与页面交互进行编辑。
下一章我们将学习如何将回调函数应用到画图中。