基本的Dash回调
回调:Dash应用程序具有交互性
Dash应用布局
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='my-id', value=u'初始值', type='text'),
html.Div(id='my-div')
])
@app.callback(
Output(component_id='my-div', component_property='children'),
[Input(component_id='my-id', component_property='value')]
)
def update_output_div(input_value):
return 'You\'ve entered "{}"'.format(input_value)
if __name__ == '__main__':
app.run_server(debug=True)
在文本框中输入内容,输出组件的子项随之立即更新。让我们分解这里发生的事情:
- 应用程序接口的“输入”和“输出”通过app.callback装饰器以声明方式描述。
- 在Dash中,我们的应用程序的输入和输出只是特定组件的属性。在此示例中,我们的输入是具有ID“ my-id” 的组件的“value”属性。我们的输出是具有ID“ my-div” 的组件的“children”属性。
- 每当输入属性发生更改时,将自动调用回调装饰器包装的函数。Dash为函数提供输入属性的新值作为输入参数,Dash使用函数返回的内容更新输出组件的属性。
- 不要将dash.dependencies.Input对象与 dash_core_components.Input对象混淆。前者仅用于这些回调,后者是实际组件。
Dash设置组件属性值的过程
当Dash应用程序启动时,它会自动使用输入组件的初始值调用所有回调,以填充输出组件的初始状态。在这个例子中,如果你指定了类似的东西html.Div(id=‘my-div’, children=‘Hello world’) ,它会在应用程序启动时被覆盖。
类似使用Microsoft Excel进行编程:每当输入单元格发生更改时,依赖于该单元格的所有单元格都将自动更新。这称为“反应式编程”(Reactive Programming)。
还记得每个组件是如何通过其关键字参数集完全描述的吗?那些属性现在很重要。通过Dash交互,我们可以通过回调函数动态更新任何这些属性。我们经常会更新该组件的children属性来显示新文本或dcc.Graph组成部分figure,以显示新的数据,但我们也可以更新style的组件,甚至可用一个的dcc.Dropdown的分量options!
dcc.Slider 更新 dcc.Graph
import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output
import pandas as pd
import plotly.graph_objs as go
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]
traces = []
for i in filtered_df.continent.unique():
df_by_continent = filtered_df[filtered_df['continent'] == i]
traces.append(go.Scatter(
x=df_by_continent['gdpPercap'],
y=df_by_continent['lifeExp'],
text=df_by_continent['country'],
mode='markers',
opacity=0.7,
marker={
'size': 15,
'line': {'width': 0.5, 'color': 'white'}
},
name=i
))
return {
'data': traces,
'layout': go.Layout(
xaxis={'type': 'log', 'title': 'GDP Per Capita'},
yaxis={'title': 'Life Expectancy', 'range': [20, 90]},
margin={'l': 40, 'b': 40, 't': 10, 'r': 10},
legend={'x': 0, 'y': 1},
hovermode='closest'
)
}
if __name__ == '__main__':
app.run_server(debug=True)
在此示例中,"value"属性Slider是app的输入,app的输出是Graph的"figure"属性。每当Slider的value变化,Dash用新值调用回调函数update_figure。该函数使用此新值过滤数据框,构造figure对象,并将其返回到Dash应用程序。
这个例子中有一些很好的模式:
- 用户使用Pandas库导入和过滤内存中的数据集。
- 用户在应用程序的开头加载数据框: df = pd.read_csv(’…’)。此数据框df处于应用程序的全局状态,可以在回调函数内部读取。
3.将数据加载到内存中可能很昂贵。通过在应用程序开始时而不是在回调函数内部加载查询数据,我们确保仅在应用程序服务器启动时执行此操作。当用户访问应用程序或与应用程序交互时,该数据(df)已在内存中。如果可能,昂贵的初始化(如下载或查询数据)应该在应用程序的全局范围内而不是在回调函数中完成。
回调不会修改原始数据,它只是通过pandas过滤器过滤来创建数据帧的副本。这很重要:你的回调不应该改变其范围之外的变量。如果您的回调修改了全局状态,那么一个用户的会话可能会影响下一个用户的会话,并且当应用程序部署在多个进程或线程上时,这些修改将不会跨会话共享
多个输入
在Dash中,任何“ Output”都可以有多个“ Input”组件。这是一个简单的示例,它将五个输入(2个Dropdown组件,2个RadioItems组件和1个Slider组件的value属性)绑定到1个输出组件(组件Graph的figure属性)。
注意:app.callback列举了第二个参数中列表的所有五个dash.dependencies.Input。
import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output
import pandas as pd
import plotly.graph_objs as go
external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
df = pd.read_csv(
'https://gist.githubusercontent.com/chriddyp/'
'cb5392c35661370d95f300086accea51/raw/'
'8e0768211f6b747c0db42a9ce9a0937dafcbd8b2/'
'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]
return {
'data': [go.Scatter(
x=dff[dff['Indicator Name'] == xaxis_column_name]['Value'],
y=dff[dff['Indicator Name'] == yaxis_column_name]['Value'],
text=dff[dff['Indicator Name'] == yaxis_column_name]['Country Name'],
mode='markers',
marker={
'size': 15,
'opacity': 0.5,
'line': {'width': 0.5, 'color': 'white'}
}
)],
'layout': go.Layout(
xaxis={
'title': xaxis_column_name,
'type': 'linear' if xaxis_type == 'Linear' else 'log'
},
yaxis={
'title': yaxis_column_name,
'type': 'linear' if yaxis_type == 'Linear' else 'log'
},
margin={'l': 40, 'b': 40, 't': 10, 'r': 0},
hovermode='closest'
)
}
if __name__ == '__main__':
app.run_server(debug=True)
在此示例中,只要元素或组件Dropdown、Slider或者RadioItems的value属性发生更改,就会调用该函数update_graph 。
update_graph函数的输入参数是每个Input属性的新值或当前值,按其指定的顺序排列。
即使一次只Input更改一个(用户只能在给定时刻更改单个Dropdown的值),Dash会收集所有指定Input属性的当前状态,并将它们传递给您的函数。您的回调函数始终保证会传递给应用程序的代表状态。
让我们扩展我们的例子,包括多个输出。
多个输出
Dash 0.39.0版本中的新功能
到目前为止,我们编写的所有回调只更新了一个Output属性。我们还可以一次更新几个:将要更新的所有属性作为列表添加到装饰器中,并从回调中返回那么多项。如果两个输出依赖于相同的计算密集型的中间结果,例如慢速数据库查询,则这尤其好。
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',
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', 'value')])
def callback_a(x):
return x**2, x**3, 2**x, 3**x, x**x
if __name__ == '__main__':
app.run_server(debug=True)
注意:组合输出并不总是一个好主意,即使你可以:
- 如果输出依赖于某些但不是所有相同的输入,则将它们分开可以避免不必要的更新。
- 如果它们具有相同的输入但使用这些输入进行独立计算,则将回调分开可以允许它们并行运行。
链式回调
用户还可以将输出和输入链接在一起:一个回调函数的输出作为另一个回调函数的输入。
此模式可用于创建动态UI,其中一个输入组件更新下一个输入组件的可用选项。
# -*- 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 = {
u'上海': [u'漕河泾', u'张江', u'创智'],
u'北京': [u'中关村', u'西二旗']
}
app.layout = html.Div([
dcc.RadioItems(
id='countries-dropdown',
options=[{'label': k, 'value': k} for k in all_options.keys()],
value=u'上海'
),
html.Hr(),
dcc.RadioItems(id='cities-dropdown'),
html.Hr(),
html.Div(id='display-selected-values')
])
@app.callback(
Output('cities-dropdown', 'options'),
[Input('countries-dropdown', 'value')])
def set_cities_options(selected_country):
return [{'label': i, 'value': i} for i in all_options[selected_country]]
@app.callback(
Output('cities-dropdown', 'value'),
[Input('cities-dropdown', 'options')])
def set_cities_value(available_options):
return available_options[0]['value']
@app.callback(
Output('display-selected-values', 'children'),
[Input('countries-dropdown', 'value'),
Input('cities-dropdown', 'value')])
def set_display_children(selected_country, selected_city):
return u'{} is a TechPark in {}'.format(
selected_city, selected_country,
)
if __name__ == '__main__':
app.run_server(debug=True)
第一个回调根据第一个RadioItems 组件中的选定值更新第二个组件中的RadioItems 可用选项。
第二个回调在options属性更改时设置初始值:它将其设置为该options数组中的第一个值。
更新逻辑:先更新画面再调用回调
最终回调显示value每个组件的选定内容。如果更改了RadioItems组件的value属性,则Dash将等待,直到value更新状态组件,然后再调用最终回调。这可以防止使用与"北京"和不一致的状态调用回调"张江"。
Dash应用程序基于一系列简单但功能强大的原则构建:声明性UI可通过反应式和功能性Python回调进行自定义。声明性组件的每个元素属性都可以通过回调进行更新,属性的子集(如value 属性)dcc.Dropdown可由用户在界面中编辑。