【呆鳥譯Py】Python交互式數(shù)據(jù)分析報(bào)告框架~Dash介紹
【呆鳥譯Py】Dash用戶指南01-02_安裝與應(yīng)用布局
【呆鳥譯Py】Dash用戶指南03_交互性簡介
【呆鳥譯Py】Dash用戶指南04_交互式數(shù)據(jù)圖
【呆鳥譯Py】Dash用戶指南05_使用State進(jìn)行回調(diào)
3. 交互性簡介
本教程的第一部分介紹了Dash應(yīng)用布局 ,第二部分將介紹如何實(shí)現(xiàn)Dash應(yīng)用的交互。
下面先看一個(gè)例子。
Dash應(yīng)用的交互式布局
import dash
from dash.dependencies import Input, Output
import dash_core_components as dcc
import dash_html_components as html
app = dash.Dash()
app.css.append_css(
{"external_url": "https://codepen.io/chriddyp/pen/bWLwgP.css"})
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)
if __name__ == '__main__':
app.run_server()

在文本框中輸入文字,輸出組件的子項(xiàng)會(huì)立即更新。下面說明這個(gè)例子后臺(tái)的每個(gè)操作步驟:
-
app.callback裝飾器通過聲明描述應(yīng)用界面的“輸入”與“輸出”項(xiàng)。 - Dash應(yīng)用的輸入、輸出項(xiàng)是指定組件的特性。本例中,輸入項(xiàng)是ID名為
my-id組件的value特性。 輸出項(xiàng)是ID名為my-div組件的children特性。 - Dash提供了輸入項(xiàng)特性改變時(shí),能夠自動(dòng)調(diào)用
callback裝飾器打包的函數(shù)。輸入項(xiàng)特性的值更新后,可以作為輸入項(xiàng)參數(shù),然后返回該函數(shù)的輸入內(nèi)容。 -
component_id與component_property關(guān)鍵字是可選的,這些對(duì)象只有兩個(gè)參數(shù)。本例中為了便于理解,列出了這兩個(gè)關(guān)鍵字,正常情況下,為了讓代碼簡明、易讀,可以省略這兩個(gè)關(guān)鍵字。 - 不要混淆
dash.dependencies.Input與dash_core_components.Input對(duì)象。前者只在回調(diào)函數(shù)中使用,后者才是真正的組件。 - 不要在
layout中設(shè)置my-div組件的children特性。Dash應(yīng)用啟動(dòng)時(shí)會(huì)自動(dòng)調(diào)用所有回調(diào)函數(shù),獲取輸入組件中的初始值,使之轉(zhuǎn)化為輸出組件的初始狀態(tài)。本例中,如果指定了html.Div(id='my-div', children='Hello world')等內(nèi)容,應(yīng)用啟動(dòng)時(shí)會(huì)被覆蓋。
這種方式類似于用Excel編程,單元格的內(nèi)容發(fā)生變化時(shí),所有與該單元格相關(guān)的單元格都會(huì)自動(dòng)更新,這就是所謂的“響應(yīng)式編程”。
請(qǐng)記住如何用關(guān)鍵字參數(shù)描述組件,這點(diǎn)非常重要。通過Dash的交互性,可以使用回調(diào)函數(shù)動(dòng)態(tài)更新這些特性。Dash可以更新組件的children特性從而顯示更新的文本,也可以通過dcc.Graph組件的figure特性展示更新的數(shù)據(jù),還可以更新組件的style,甚至是dcc.Dropdown組件的options。
下面這個(gè)例子通過dcc.Slider更新dcc.Gragh:
import dash
import dash_core_components as dcc
import dash_html_components as html
import plotly.graph_objs as go
import pandas as pd
df = pd.read_csv(
'https://raw.githubusercontent.com/plotly/'
'datasets/master/gapminderDataFiveYear.csv')
app = dash.Dash()
app.css.append_css(
{"external_url": "https://codepen.io/chriddyp/pen/bWLwgP.css"})
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(),
step=None,
marks={str(year): str(year) for year in df['year'].unique()}
)
])
@app.callback(
dash.dependencies.Output('graph-with-slider', 'figure'),
[dash.dependencies.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'},
yaxis={'title': '平均壽命', '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()

本例中,Slider的value特性是Dash應(yīng)用的輸入項(xiàng),輸出項(xiàng)是Graph的figure特性。Slider的value變更時(shí),Dash調(diào)用update_figure回調(diào)函數(shù)獲取更新值。這個(gè)函數(shù)會(huì)篩選DataFrame生成新的值,創(chuàng)建figure 對(duì)象,并將其返回至Dash應(yīng)用。
以下是本例的核心內(nèi)容:
- 使用Pandas導(dǎo)入并篩選內(nèi)存中的數(shù)據(jù)集;
- 應(yīng)用啟動(dòng)時(shí),加載DataFrame:
df = pd.read_csv('...')。在這個(gè)Dash應(yīng)用里,DataFramedf是全局的,可以被回調(diào)函數(shù)讀取。 - 將數(shù)據(jù)加載至內(nèi)存并進(jìn)行計(jì)算的代價(jià)很高,所以要在應(yīng)用啟動(dòng)時(shí)載入數(shù)據(jù),避免在回調(diào)函數(shù)中加載數(shù)據(jù),確保用戶訪問或與應(yīng)用交互時(shí),數(shù)據(jù)(即
df)已經(jīng)載入至內(nèi)存。盡量在應(yīng)用的全局范圍內(nèi)下載或查詢數(shù)據(jù)等大規(guī)模數(shù)據(jù)初始化操作,避免在回調(diào)函數(shù)里進(jìn)行這類操作。 - 回調(diào)函數(shù)不會(huì)修改原始數(shù)據(jù),只是通過Pandas的過濾器來篩選數(shù)據(jù),并創(chuàng)建DataFrame的副本。這點(diǎn)非常重要:不要在回調(diào)函數(shù)范圍之外更改變量。如果在全局狀態(tài)下調(diào)整回調(diào)函數(shù),某一用戶的會(huì)話就可能影響下一用戶的會(huì)話,特別是應(yīng)用部署在多進(jìn)程或多線程的環(huán)境時(shí),這些修改將導(dǎo)致跨會(huì)話數(shù)據(jù)分享出現(xiàn)問題。
多重輸入
任一Dash**輸出項(xiàng) 都可對(duì)應(yīng)多個(gè)輸入項(xiàng) **。下面這個(gè)例子為某個(gè)輸出組件(Graph 組件的figure特性)綁定了5個(gè)輸入項(xiàng)(2個(gè)下拉菜單Dropdown 組件,兩個(gè)單選按鈕RadioItems組件,還有1個(gè)滑動(dòng)條Slider組件)。注意?在第2個(gè)參數(shù)里,app.callback 是如何在一個(gè)列表中列出5個(gè)dash.dependenceies.Input輸入項(xiàng)的。
import dash
import dash_core_components as dcc
import dash_html_components as html
import plotly.graph_objs as go
import pandas as pd
app = dash.Dash()
app.css.append_css(
{"external_url": "https://codepen.io/chriddyp/pen/bWLwgP.css"})
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(),
step=None,
marks={str(year): str(year) for year in df['Year'].unique()}
)
])
@app.callback(
dash.dependencies.Output('indicator-graphic', 'figure'),
[dash.dependencies.Input('xaxis-column', 'value'),
dash.dependencies.Input('yaxis-column', 'value'),
dash.dependencies.Input('xaxis-type', 'value'),
dash.dependencies.Input('yaxis-type', 'value'),
dash.dependencies.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()

本例中,Dropdown、Slider、RadioItems這些組件的value特性變化時(shí),就會(huì)調(diào)用update_graph函數(shù)。
update_graph的輸入?yún)?shù)就是這些組件Input特性的當(dāng)前值或更新值,其優(yōu)先級(jí)為它們的指定順序。
雖然同一時(shí)間內(nèi),只能修改一個(gè)Input特性(比如用戶一次只能修改一個(gè)下拉菜單的值),但時(shí),Dash會(huì)采集所有綁定組件Input 特性的當(dāng)前值,并通過函數(shù)傳遞給回調(diào)函數(shù),確??偰塬@得該應(yīng)用當(dāng)前狀態(tài)的值。
下面在這個(gè)例子的基礎(chǔ)上加入多重輸出。
多重輸出
一個(gè)Dash回調(diào)函數(shù)僅能更新一個(gè)輸出屬性。要想實(shí)現(xiàn)多重輸出,需要編寫多個(gè)函數(shù)。
import dash
import dash_core_components as dcc
import dash_html_components as html
app = dash.Dash('')
app.css.append_css(
{"external_url": "https://codepen.io/chriddyp/pen/bWLwgP.css"})
app.layout = html.Div([
dcc.RadioItems(
id='dropdown-a',
options=[{'label': i, 'value': i} for i in ['北京', '天津', '上海']],
value='北京'
),
html.Div(id='output-a'),
dcc.RadioItems(
id='dropdown-b',
options=[{'label': i, 'value': i} for i in ['東城區(qū)', '西城區(qū)', '朝陽區(qū)']],
value='朝陽區(qū)'
),
html.Div(id='output-b')
])
@app.callback(
dash.dependencies.Output('output-a', 'children'),
[dash.dependencies.Input('dropdown-a', 'value')])
def callback_a(dropdown_value):
return '已選中"{}"'.format(dropdown_value)
@app.callback(
dash.dependencies.Output('output-b', 'children'),
[dash.dependencies.Input('dropdown-b', 'value')])
def callback_b(dropdown_value):
return '已選中"{}"'.format(dropdown_value)
if __name__ == '__main__':
app.run_server()

可以將輸入與輸出項(xiàng)鏈在一起:一個(gè)回調(diào)函數(shù)的輸出項(xiàng)可以是另一個(gè)回調(diào)函數(shù)的輸入項(xiàng)。
這個(gè)模式可以用來創(chuàng)建動(dòng)態(tài)UI,一個(gè)輸入組件可以更新另一個(gè)輸入組件的可用選項(xiàng),請(qǐng)看下面的例子。
# -*- coding: utf-8 -*-
import dash
from dash.dependencies import Input, Output
import dash_core_components as dcc
import dash_html_components as html
app = dash.Dash(__name__)
app.css.append_css(
{"external_url": "https://codepen.io/chriddyp/pen/bWLwgP.css"})
all_options = {
'北京': ['東城區(qū)', '西城區(qū)', '朝陽區(qū)'],
'上海': ['黃浦區(qū)', '靜安區(qū)', '普陀區(qū)']
}
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(
dash.dependencies.Output('cities-dropdown', 'options'),
[dash.dependencies.Input('countries-dropdown', 'value')])
def set_cities_options(selected_country):
return [{'label': i, 'value': i} for i in all_options[selected_country]]
@app.callback(
dash.dependencies.Output('cities-dropdown', 'value'),
[dash.dependencies.Input('cities-dropdown', 'options')])
def set_cities_value(available_options):
return available_options[0]['value']
@app.callback(
dash.dependencies.Output('display-selected-values', 'children'),
[dash.dependencies.Input('countries-dropdown', 'value'),
dash.dependencies.Input('cities-dropdown', 'value')])
def set_display_children(selected_country, selected_city):
return '{}是{}的轄區(qū)。'.format(
selected_city, selected_country,
)
if __name__ == '__main__':
app.run_server(debug=True)

第二個(gè)單選按鈕RadioItems的選項(xiàng)基于第一個(gè)回調(diào)函數(shù)傳遞的單選按鈕RadioItems中選擇的值。
第二個(gè)回調(diào)函數(shù)設(shè)置了options特性改變時(shí)的初始值:它將自身設(shè)置為options數(shù)組中的第一個(gè)值。
最后的回調(diào)函數(shù)顯示了每個(gè)組件中的可選值value。如果改變城市單選按鈕RadioItems的值value,Dash會(huì)等城區(qū)單選按鈕RadioItems 的值value更新后,再調(diào)用最終的回調(diào)函數(shù)。
小結(jié)
本節(jié)介紹了Dash回調(diào)函數(shù)的基本概念。Dash應(yīng)用是基于下述簡單但強(qiáng)大的原則進(jìn)行構(gòu)建的:可以通過響應(yīng)式與函數(shù)式的Python回調(diào)函數(shù)自定義聲明式的UI。聲明式組件中的每個(gè)元素屬性都可以通過回調(diào)函數(shù)和屬性子集進(jìn)行更新,比如dcc.Dropdown的value特性,這樣用戶就可以在交互界面中進(jìn)行編輯。
下一章將闡述如何使用上述規(guī)則,通過dash_core-componets.Graph組件讓Dash應(yīng)用響應(yīng)頁面上的圖形交互功能。
【呆鳥譯Py】Python交互式數(shù)據(jù)分析報(bào)告框架~Dash介紹
【呆鳥譯Py】Dash用戶指南01-02_安裝與應(yīng)用布局
【呆鳥譯Py】Dash用戶指南03_交互性簡介
【呆鳥譯Py】Dash用戶指南04_交互式數(shù)據(jù)圖
【呆鳥譯Py】Dash用戶指南05_使用State進(jìn)行回調(diào)