Dash 回调与交互#
参考:Basic Callbacks | Dash for Python Documentation | Plotly
在 布局 中,我们了解到 app.layout
描述应用程序的外观,并且是组件的分层树。html
库提供了所有 HTML 标记的类,关键字参数描述了 HTML 属性,例如 style
,className
和 id
。dcc
库生成更高级别的组件,如控件和图形。本章介绍如何使用回调函数制作 Dash 应用程序:每当输入组件的属性发生更改时 Dash 会自动调用的 Python 函数。
为了获得最佳的用户交互和图表加载性能,生产 Dash 应用程序应考虑 Dash Enterprise 的 Job Queue,HPC,Datashader 和 horizontal scaling 功能。
让我们从一个交互式 Dash 应用程序的简单示例开始。
Dash 简单的交互#
from dash import dcc, html
from dash.dependencies import Input, Output
from app import app
import dash
dash.register_page(__name__)
layout = html.Div([
html.H6("更改文本框中的值以查看回调操作!"),
html.Div(["输入:",
dcc.Input(id='my-input', 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 f'输出:{input_value}'
让我们拆解这个例子:
我们将应用程序接口的
"inputs"
和"outputs"
声明性地描述为@app.callback
装饰器的参数。
小技巧
关于 @app.callback
装饰器
通过编写此装饰器,我们告诉 Dash 每当 “input” 组件(文本框)的值更改时为我们调用此函数,以便更新页面上 “output” 组件的子级(HTML div)。
您可以为
@app.callback
装饰器包装的函数使用任何名称。约定是该名称描述了回调输出。您可以为函数参数使用任何名称,但是必须像在常规 Python 函数中一样在回调函数中使用与定义时相同的名称。参数是位置性的:首先以与装饰器中相同的顺序给出
Input
项,然后给出任何State
项。当引用它作为
@app.callback
装饰器的输入或输出时,必须使用与给app.layout
中的 Dash 组件相同的 ID。@app.callback
装饰器需要直接位于回调函数声明的上方。如果装饰器和函数定义之间有空白行,则回调注册将不会成功。如果您对装饰器语法的含义感到好奇,可以阅读此 StackOverflow 答案,并通过阅读 PEP 318-函数和方法的装饰器 来了解有关装饰器的更多信息。
在 Dash 中,我们应用程序的输入和输出只是特定组件的属性。在此示例中,我们的输入是ID为
"my-input"
的组件的"value"
属性。我们的输出是 ID 为"my-output"
的组件的"children"
属性。每当输入属性更改时,回调装饰器包装的函数将自动被调用。Dash 为函数提供输入属性的新值作为输入参数,Dash 使用函数返回的值更新输出组件的属性。
component_id
和component_property
关键字是可选的(每个对象只有两个参数)。为了清楚起见,它们包含在此示例中,但是为了简洁和易读起见,在本文档的其余部分中将省略它们。不要混淆
dash.dependencies.Input
对象和dash_core_components.Input
对象。前者仅用于这些回调中,而后者是实际组件。注意,我们没有在布局中为
my-output
组件的children
属性设置值。Dash 应用程序启动时,它将自动使用输入组件的初始值调用所有回调,以填充输出组件的初始状态。在此示例中,如果您指定了类似html.Div(id='my-output', children='Hello world')
的名称,则在应用启动时它将被覆盖。
这有点像使用 Microsoft Excel 进行编程:每当输入单元格发生更改时,依赖于该单元格的所有单元格都会自动更新。这称为“反应式编程”(“Reactive Programming”)。
还记得每个组件是如何通过其一组关键字参数进行完整描述的吗?这些属性现在很重要。借助 Dash 交互性,我们可以通过回调函数动态更新这些属性中的任何一个。通常,我们将更新组件的 children
以显示新文本,或者更新 dcc.Graph
组件的 figure
以显示新数据,但是我们还可以更新组件的 style
,甚至更新 dcc.Dropdown
组件可用的 options
!
让我们看一下另一个示例,其中 dcc.Slider
更新了 dcc.Graph
。
Dash Figure 和 Slider 交互#
import pandas as pd
from dash import dcc, html
import plotly.express as px
from dash.dependencies import Input, Output
from sanstyle.github.file import lfs_url
from app import app
import dash
dash.register_page(__name__)
url = lfs_url('SanstyleLab/plotly-dastsets',
'gapminderDataFiveYear.csv')
df = pd.read_csv(url)
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
在此示例中,Slider
的 "value"
属性是应用程序的输入,而应用程序的输出则是 Graph
的 "figure"
属性。每当 Slider
的值更改时,Dash 就会使用新值调用回调函数 update_figure
。该函数使用此新值过滤数据框,构造 figure
对象,并将其返回给 Dash 应用程序。
此示例中有一些不错的模式:
我们正在使用 Pandas 库来导入和过滤内存中的数据集。
我们在应用程序的开头加载数据帧:
df = pd.read_csv('...')
。此数据框df
处于应用程序的全局状态,可以在回调函数中读取。将数据加载到内存中可能会很昂贵。通过在应用程序的开始而不是在回调函数内部加载查询数据,我们确保仅在应用程序服务器启动时执行此操作。当用户访问该应用程序或与该应用程序进行交互时,该数据(
df
)已经在内存中。如果可能,应在应用程序的全局范围内而不是在回调函数内完成昂贵的初始化(如下载或查询数据)。回调不会修改原始数据,它只是通过
pandas
过滤器进行过滤来创建数据帧的副本。这很重要:您的回调函数绝不要在变量范围之外进行变量的更改。如果您的回调修改了全局状态,则一个用户的会话可能会影响下一个用户的会话,并且当应用程序部署在多个进程或线程上时,这些修改将不会在各个会话之间共享。我们正在使用
layout.transition
打开transitions
,以了解数据集如何随时间演变:transitions
允许图表从一个状态平滑地更新到下一个状态,就好像它是动态的一样。
Dash 多输入的回调#
在 Dash 中,任何 "Output"
可以具有多个 "Input"
组件。这是一个简单的示例,它将五个 Inputs(2个Dropdown
组件,2个RadioItems
组件和1个Slider
组件的value
属性)绑定到1个 Output 组件(Graph
组件的figure
属性)。请注意app.callback
如何在第二个参数的列表内列出所有五个dash.dependencies.Input
。
import pandas as pd
from dash import dcc, html
import plotly.express as px
from dash.dependencies import Input, Output
from sanstyle.github.file import lfs_url
from app import app
from sanstyle.github.file import lfs_url
import dash
dash.register_page(__name__)
url = lfs_url('SanstyleLab/plotly-dastsets',
'country_indicators.csv')
df = pd.read_csv(url)
available_indicators = df['Indicator Name'].unique()
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
在此示例中,只要Dropdown
,Slider
或RadioItems
组件的value
属性发生更改,就会调用 update_graph
函数。
按指定顺序,update_graph
函数的输入参数是每个Input
属性的新值或当前值。
即使一次仅更改一个Input
(用户只能在给定的时刻更改单个Dropdown
的值),Dash 仍会收集所有指定Input
属性的当前状态并将其传递给您的函数。您的回调函数始终保证传递给应用程序代表状态。
让我们扩展示例以包括多个输出。
Dash 多输出的回调#
到目前为止,我们编写的所有回调仅更新单个Output
属性。我们也可以一次更新几个:将要更新的所有属性作为列表放置在装饰器中,并从回调中返回那么多项。如果两个输出依赖于相同的计算密集型中间结果(例如慢速数据库查询),则特别好。
from dash import dcc, html
from dash.dependencies import Input, Output
from app import app
import dash
dash.register_page(__name__)
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
提醒您:即使您可以合并输出,也不总是一个好主意:
如果输出依赖于某些而非全部相同的输入,则将它们分开可以避免不必要的更新。
如果它们具有相同的输入,但使用这些输入进行独立的计算,则将回调分开设置可以使它们并行运行。
Dash 链式回调#
您也可以将输出和输入链接在一起:一个回调函数的输出可以是另一个回调函数的输入。
此模式可用于创建动态 UI,其中一个输入组件将更新下一个输入组件的可用选项。这是一个简单的例子。
from dash import dcc, html
from dash.dependencies import Input, Output
from app import app
import dash
dash.register_page(__name__)
all_options = {
'America': ['New York City', 'San Francisco', 'Cincinnati'],
'Canada': [u'Montréal', 'Toronto', 'Ottawa']
}
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 f'{selected_city} is a city in {selected_country}'
第一个回调根据第一个RadioItems
组件中的选定值更新第二个RadioItems
组件中的可用选项。
当options
属性更改时,第二个回调将设置一个初始值:它将其设置为该options
数组中的第一个值。
最后的回调显示每个组件的选定value
。如果更改国家RadioItems
组件的value
,则 Dash 将等待,直到更新了城市组件的值,然后才调用最后的回调。这样可以防止以"America"
和"Montréal"
之类的不一致状态调用您的回调。
带状态的 Dash 应用#
在某些情况下,您的应用程序中可能会有“表单”类型的模式。在这种情况下,您可能希望读取输入组件的值,但是仅当用户完成了以表格形式输入其所有信息时才可以。
将回调直接附加到输入值可以看起来像这样:
from dash import dcc, html
from dash.dependencies import Input, Output
from app import app
import dash
dash.register_page(__name__)
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 f'Input 1 is "{input1}" and Input 2 is "{input2}"'
在此示例中,只要 dash.dependencies.Input
描述的任何属性发生更改,就会触发回调函数。在上面的输入中输入数据,自己尝试一下。
dash.dependencies.State
允许您传递额外的值而无需触发回调。这是与上述相同的示例,但 dcc.Input
为 dash.dependencies.State
,按钮为 dash.dependencies.Input
。
from dash import dcc, html
from dash.dependencies import Input, Output, State
from app import app
import dash
dash.register_page(__name__)
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 f'''
The Button has been pressed {n_clicks} times,
Input 1 is "{input1}",
and Input 2 is "{input2}"
'''
在此示例中,在 dcc.Input
框中更改文本不会触发回调,但单击按钮将起作用。dcc.Input
值的当前值仍会传递到回调中,即使它们不会触发回调函数本身。
请注意,通过侦听 html.Button
组件的 n_clicks
属性来触发回调。n_clicks
是一个属性,每次单击该组件时该属性都会增强。它在 dash_html_components
库中的每个组件中都可用。
小结#
我们已经介绍了 Dash 中回调的基础。Dash 应用程序是基于一组简单但功能强大的原则构建的:声明性 UI,可通过反应性(reactive)和功能性(functional )Python 回调进行自定义。声明性组件的每个元素属性都可以通过回调进行更新,并且该属性的子集(例如 dcc.Dropdown
的 value
属性)可以由用户在界面中进行编辑。