【Dash系列】Python的Web可视化框架Dash(1)---简介
【Dash系列】Python的Web可视化框架Dash(2)---安装
【Dash系列】Python的Web可视化框架Dash(3)---布局设置
【Dash系列】Python的Web可视化框架Dash(4)---基本回调
【Dash系列】Python的Web可视化框架Dash(5)---状态和预更新
【Dash系列】Python的Web可视化框架Dash(6)---交互和过滤
【Dash系列】Python的Web可视化框架Dash(7)---回调共享
【Dash系列】Python的Web可视化框架Dash(8)---核心组件
本节介绍如何实现Dash应用的回调,先导入本节用到的所有包
import pandas as pd
import plotly.graph_objs as go
import dash
import dash_core_components as dcc # 交互式组件
import dash_html_components as html # 代码转html
from dash.dependencies import Input, Output # 回调
from jupyter_plotly_dash import JupyterDash # Jupyter中的Dash
一、交互式布局
(一) 代码
app = JupyterDash('Dash Layout')
app.layout = html.Div([
dcc.Input(id = 'my-id', value = '初始值', 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 '你输入了"{}"'.format(input_value)
app
(二)效果图
(三)说明
在文本框中输入文字,输出组件的子项会立即更新。效果图显示,第一个设置的默认值,后两个,分布输入了数值100和字符串Dash;
app.callback 装饰器通过声明,描述应用程序界面的“输入”与“输出”项;
Dash中应用程序的【输入】和【输出】只是特定组件的属性。本例中,输入项是ID名为my-id 组件的value特性。 输出项是ID名为my-div 组件的children特性;
当输入项组件的属性值,发生更改时,将自动调用callback装饰器打包的函数,将更新的内容,作为输入项参数,返回函数的输入内容,更新输出项组件的属性值;
【输入项】和【输出项】对象的关键字参数 component_id 和 component_property,都是可选的。本例中,为了便于理解,列出了这两个关键字,通常情况下,为了让代码简明、易读,可以省略这两个关键字;
不要混淆 dash.dependencies.Input 与dash_core_components.Input对象。前者只在回调函数中使用,后者才是真正的组件;
Dash应用程序启动时,会自动使用输入组件的初始值,调用所有的回调函数,以填充输出组件的初始值。所以,不要在layout中设置 my-div组件的children特性,本例中,如果指定了 html.Div(id='my-div', children='Hello world') 的内容,应用启动时会被覆盖。这种方式类似于Microsoft Excel编程:当单元格的内容发生变化时,依赖于该单元格的所有单元格的内容,都将自动更新。这称为 “反应式编程” (Reactive Programming) 。
二、滑动条
(一) 代码
# 数据源
df = pd.read_csv(
'https://raw.githubusercontent.com/plotly/'
'datasets/master/gapminderDataFiveYear.csv')
# 设置Dash应用程序
app = JupyterDash('Slider Update Gragh')
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 val in filtered_df.continent.unique():
df_by_continent = filtered_df[filtered_df.continent == val]
traces.append(go.Scatter(
x = df_by_continent['gdpPercap'],
y = df_by_continent['lifeExp'],
text = df_by_continent['country'],
name = val,
mode = 'markers',
opacity = 0.8,
marker = dict(size = 15, line = dict(width = 0.5, color = 'white'))
))
fig = dict(
data = traces,
layout = go.Layout(
xaxis = dict(type = 'log', title = '人均GDP'),
yaxis = dict(title = '平均寿命', range = [20, 90]),
margin = dict(l = 40, b = 40, t = 10, r = 10),
hovermode = 'closest'
)
)
return fig
app
(二) 效果图
(三)说明
本例中,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的副本。这点非常重要:不要在回调函数范围之外更改变量。如果在全局状态下调整回调函数,某一用户的会话就可能影响下一用户的会话,特别是应用部署在多进程或多线程的环境时,这些修改可能会导致跨会话数据分享出现问题;
三、多重输入
(一) 代码
# 数据
df = pd.read_csv(
'https://gist.githubusercontent.com/chriddyp/'
'cb5392c35661370d95f300086accea51/raw/'
'8e0768211f6b747c0db42a9ce9a0937dafcbd8b2/'
'indicators.csv')
# 设置Dash
app = JupyterDash('many input')
app.layout = html.Div([
html.Div([
html.Div([
dcc.Dropdown(
id = 'xaxis-column',
options = [{'label': i, 'value': i} for i in df['Indicator Name'].unique()],
value = 'Fertility rate, total (births per woman)'),
dcc.RadioItems(
id = 'xaxis-type',
options = [{'label': i, 'value': i} for i in ['线性', '日志']],
value = '线性',
labelStyle = dict(display = 'inline-block'))],
style = dict(width = '48%', display = 'inline-block')
),
html.Div([
dcc.Dropdown(
id = 'yaxis-column',
options = [{'label': i, 'value': i} for i in df['Indicator Name'].unique()],
value = 'Life expectancy at birth, total (years)'),
dcc.RadioItems(
id = 'yaxis-type',
options = [{'label': i, 'value': i} for i in ['线性', '日志']],
value = '线性',
labelStyle = dict(display = 'inline-block'))],
style = dict(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]
result = dict(
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 = dict(title = xaxis_column_name, type = 'linear' if xaxis_type == '线性' else '日志'),
yaxis = dict(title = yaxis_column_name, type = 'linear' if yaxis_type == '线性' else '日志'),
margin = {'l': 40, 'b': 40, 't': 10, 'r': 0},
hovermode = 'closest'
)
)
return result
app
(二) 效果图
(三)说明
在Dash中,任何“ Output”都可以有多个“ Input”组件;
本例中,将五个输入组件:2个下拉菜单(Dropdown)组件、2个单选按钮(RadioItems)组件、1个滑动条(Slider)组件,绑定到1个输出组件(Graph 组件的figure特性);
回调函数的第二个参数,列表中列举了所有的五个输入项dash.dependencies.Input ;
示例中,Dropdown、Slider、RadioItems这些组件的value特性变化时,就会调用update_graph函数;
update_graph函数的输入参数,就是这些组件Input特性的当前值或更新值,按其指定的顺序排列;
即使每次只修改一个Input特性,比如用户一次只能修改一个下拉菜单的值,但Dash会采集所有绑定组件Input 特性的当前值,并传递给回调函数,确保总能获得该应用当前状态的值。
四、多重输出
(一) 代码
app = JupyterDash('many output')
app.layout = html.Div([
dcc.RadioItems(
id = 'button-a',
options = [{'label': i, 'value': i} for i in ['北京', '天津', '上海']],
value = '北京'),
html.Div(id = 'output-a'),
dcc.RadioItems(
id = 'button-b',
options = [{'label': i, 'value': i} for i in ['东城区', '西城区', '朝阳区']],
value = '朝阳区'),
html.Div(id = 'output-b')
])
@app.callback(
Output('output-a', 'children'),
[Input('button-a', 'value')]
)
def callback_a(button_value):
return f"已选中{button_value}"
@app.callback(
Output('output-b', 'children'),
[Input('button-b', 'value')]
)
def callback_a(button_value):
return f"已选中{button_value}"
app
(二) 效果图
(三)说明
一个Dash回调函数只能更新一个输出属性。要想实现多重输出,需要编写多个函数;
具体方法:将需要更新的所有属性,作为列表添加到装饰器中,并从回调中返回多个输出项。如果两个输出依赖于相同的计算密集型中间结果,例如慢速数据库查询,推荐使用该方法;
组合输出并不总是一个好主意:1)如果输出依赖于某些但不是所有相同的输入,则将它们分开可以避免不必要的更新;2)如果它们具有相同的输入,但使用这些输入进行独立计算,则将回调分开,可以实现并行运行它们;
五、链式回调
(一) 代码
app = JupyterDash('Chained Callbacks')
all_options = {
'北京': ['东城区', '西城区', '朝阳区'],
'上海': ['黄浦区', '静安区', '普陀区']
}
app.layout = html.Div([
dcc.RadioItems(
id = 'countries-dropdown',
options = [{'label': k, 'value': k} for k in all_options.keys()],
value = '北京'),
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(select_country):
return [{'label': i, 'value': i} for i in all_options[select_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(select_country, select_city):
return f"{select_city}是{select_country}的辖区。"
app
(二) 效果图
(三)说明
- 链式回调:将输出和输入链接在一起,即一个回调函数的输出是另一个回调函数的输入;
- 此模式用于创建动态UI,其中一个输入组件更新下一个输入组件的可用选项;
- 第二个单选按钮RadioItems的选项,基于第一个回调函数传递的单选按钮RadioItems中选择的值;
- 第二个回调函数设置了options特性改变时的初始值:将自身设置为options数组中的第一个值;
- 最后的回调函数,显示了每个组件中的可选内容。如果更改了城市单选按钮RadioItems组件的value属性,则Dash将等待,直到value更新状态组件后,再调用最后的回调函数。
六、小结
- Dash应用是基于下述简单但强大的原则进行构建的:通过响应式与函数式的Python回调函数,自定义声明式的UI;
- 声明式组件中的每个元素属性,都可以通过回调函数和属性子集进行更新,比如dcc.Dropdown的value特性,这样用户就可以在交互界面中进行编辑。