Python的Web可視化框架Dash(4)---基本回調

【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

(二)效果圖

(三)說明

  1. 在文本框中輸入文字,輸出組件的子項會立即更新。效果圖顯示,第一個設置的默認值,后兩個,分布輸入了數值100和字符串Dash;

  2. app.callback 裝飾器通過聲明,描述應用程序界面的“輸入”與“輸出”項;

  3. Dash中應用程序的【輸入】和【輸出】只是特定組件的屬性。本例中,輸入項是ID名為my-id 組件的value特性。 輸出項是ID名為my-div 組件的children特性;

  4. 當輸入項組件的屬性值,發生更改時,將自動調用callback裝飾器打包的函數,將更新的內容,作為輸入項參數,返回函數的輸入內容,更新輸出項組件的屬性值;

  5. 【輸入項】和【輸出項】對象的關鍵字參數 component_idcomponent_property,都是可選的。本例中,為了便于理解,列出了這兩個關鍵字,通常情況下,為了讓代碼簡明、易讀,可以省略這兩個關鍵字;

  6. 不要混淆 dash.dependencies.Inputdash_core_components.Input對象。前者只在回調函數中使用,后者才是真正的組件;

  7. 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

(二) 效果圖

(三)說明

  1. 本例中,app的輸入是 Slider 的屬性 value,app的輸出是 Graph 的屬性 figure。當 Slider 的 value 變化時,Dash用新值調用回調函數 update_figure,該函數使用此新值過濾數據框,構造 figure 對象,并將其返回到Dash應用程序中,作為輸出;

  2. 使用關鍵字參數進行組件描述,很重要。通過Dash交互性,使用回調函數,可以動態地更新這些特性。如:更新組件的 children 屬性從而更新文本內容、更新 dcc.Graph 組件的 figure 屬性從而更新數據、更新組件的 style 屬性從而更新畫布樣式、更新 dcc.Dropdown 組件的 options 從而更新下拉菜單;

  3. 將數據加載至內存并進行計算的代價很高,所以盡量在應用的全局范圍內下載或查詢數據,避免在回調函數里進行這類操作,確保用戶訪問或與應用交互時,數據(df)已經載入至內存。本例中 df 獲取的數據是全局的,可以被回調函數讀取;

  4. 回調函數不會修改原始數據,只是通過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

(二) 效果圖

(三)說明

  1. 在Dash中,任何“ Output”都可以有多個“ Input”組件;

  2. 本例中,將五個輸入組件:2個下拉菜單(Dropdown)組件、2個單選按鈕(RadioItems)組件、1個滑動條(Slider)組件,綁定到1個輸出組件(Graph 組件的figure特性);

  3. 回調函數的第二個參數,列表中列舉了所有的五個輸入項dash.dependencies.Input ;

  4. 示例中,Dropdown、Slider、RadioItems這些組件的value特性變化時,就會調用update_graph函數;

  5. update_graph函數的輸入參數,就是這些組件Input特性的當前值或更新值,按其指定的順序排列;

  6. 即使每次只修改一個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

(二) 效果圖

(三)說明

  1. 一個Dash回調函數只能更新一個輸出屬性。要想實現多重輸出,需要編寫多個函數;

  2. 具體方法:將需要更新的所有屬性,作為列表添加到裝飾器中,并從回調中返回多個輸出項。如果兩個輸出依賴于相同的計算密集型中間結果,例如慢速數據庫查詢,推薦使用該方法;

  3. 組合輸出并不總是一個好主意: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

(二) 效果圖

(三)說明

  1. 鏈式回調:將輸出和輸入鏈接在一起,即一個回調函數的輸出是另一個回調函數的輸入;
  2. 此模式用于創建動態UI,其中一個輸入組件更新下一個輸入組件的可用選項;
  3. 第二個單選按鈕RadioItems的選項,基于第一個回調函數傳遞的單選按鈕RadioItems中選擇的值;
  4. 第二個回調函數設置了options特性改變時的初始值:將自身設置為options數組中的第一個值;
  5. 最后的回調函數,顯示了每個組件中的可選內容。如果更改了城市單選按鈕RadioItems組件的value屬性,則Dash將等待,直到value更新狀態組件后,再調用最后的回調函數。

六、小結

  1. Dash應用是基于下述簡單但強大的原則進行構建的:通過響應式與函數式的Python回調函數,自定義聲明式的UI;
  2. 聲明式組件中的每個元素屬性,都可以通過回調函數和屬性子集進行更新,比如dcc.Dropdown的value特性,這樣用戶就可以在交互界面中進行編輯。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容