top of page
  • 作家相片任性分析師 GT

Python Dash 實踐(下)—— Callback與實際案例|教學

已更新:4月18日

這次的 Dash 教學實戰將會著重在 Callback 的設計上,以及過程中遇到的疑難雜症。專案完整程式碼提供在文章最後。


前情提要

dashboard mockup using python dash
專案頁面預覽

這系列文章將會以分析師視角稍微敘述身為前後端小白的我怎麼完成這項專案。大致包含以下內容:


除了下面會提及的 Callback 與開發遇到的問題,其他內容可回到上篇閱讀:Python Dash 實踐(上)——草圖設計與CSS|教學


先簡單重申一下頁面區塊,幫助大家了解底下的說明:


dashboard build with python dash

👉 點這裡到 Demo 網站(帳號 & 密碼均為 test)



 

開始之前,先來簡單說明一下:為什麼我們會用到 Callback?以及 Callback 到底是什麼?


為什麼會用到 Callback?


由於在我們的設計裡面,是需要使用者提供客戶類別以及日期區間,使底下的財務報表根據需求呈現指定的內容。


要達到這個目的,除了做好一個下拉式選單和日期選單介面讓使用者操作,當使用者實際也點選完畢後,我們又該怎麼取得使用者所選擇的選項?


這就是需要 Callback 的地方啦!


什麼是 Callback?

functions that are automatically called by Dash whenever an input component's property changes, in order to update some property in another component (the output).

── Dash Python 官網


Callback 的作用簡單來說,就是當一個元件的 property 改變的時候(例如:網頁上的按鈕被點擊時),另一個元件將會被觸發跟著被改變(例如:圖表的資料改變、表格進行資料更新,等等)


以上述的例子,網頁按鈕即是 Input,表格資料則是 Output。這也是構成 Callback 的一個基礎,每個 Callback 都會帶有 Input(s) 與 Output(s)。而 Input 改變時,就會隨即觸發 Output 的改變。


藉由 Callback,我們便能得到使用者在下拉式選單中的選項(Input),進一步去運用在表格資料(Output)的呈現。


還是不太清楚 Callback 運用也沒關係,底下會有更多實際案例!若想知道更多案例也可參考官網教學

 

Callback 設計


使用者介面會接觸到的 Callback 為以下兩種:


#1. 展開按鈕(Open collapse)


展開 / 收合動畫呈現

👉 點這裡到 Demo 網站操作(帳號 & 密碼均為 test)


Callback 設計說明、Input 與 Output:


callback demo on dashboad mockup using python dash


Input(Trigger):Open collapse 按鈕(藍色箭頭)


Output:下方整個 html.Div 區塊(紅框)


說明:用來控制 <中間層 - 客戶收費設定> (紅框)是否展開或是收合


目的:<中間層 - 客戶收費設定> 並非主要財報區塊,使用情境多為有客戶資料須更新時才需編輯。因使用頻率不高,避免篇幅過長故設定展開按鈕預設隱藏。








@app.callback(
    Output("collapse", "is_open"), # is_open 預設為 False
    [Input("collapse-button", "n_clicks")],
    [State("collapse", "is_open")], 
)
def toggle_collapse(n, is_open):
    if n:
        return not is_open
    return is_open

➡️ 更多 Collapse 範例點這裡


 

#2. 財報內容


財報內容 Callback 範例

👉 點這裡到 Demo 網站操作(帳號 & 密碼均為 test)


Callback 設計說明、Input 與 Output:


Inputs:下拉式選單、日期選單、 <中間層 - 客戶收費設定> 表格


Trigger(Input):提交按鈕(Submit)


Output:<最下方 - 財務報表> 財報表格


Input 設定說明:可透過 <最上方 - 篩選器區塊> 中的下拉選單(客戶類別)與日期選單,以及編輯 <中間層 - 客戶收費設定> 指定財報內容。


Input 設定目的:讓報表更為彈性,並依照 <中間層 - 客戶收費設定> 規格體現到報表中。


Trigger 設定說明:財報內容會在按下提交按鈕(Submit)後更新。剛進入頁面時財報不會自動讀取數據(pre-load),會先呈現空表格。


Trigger 設定目的:為了避免讀取頁面時間過久,故取消 pre-load。也避免編輯時自動讀取數據庫,故設計點擊提交後才開始資料讀取 & 運算。


 

另外我的專案中還有一種 Callback 算是間接觸發,待會也會在最後一個篇幅(開發過程遇到的問題)進行說明。


間接觸發 Callback:存取資料


Trigger & Input:<最下方 - 財務報表> 財報表格資料


Output:dcc.Store() 資料


說明:透過 dcc.Store() 存取前一次的財報結果


目的:為了避免使用者在提交後,又進行新的篩選或編輯導致原財報成空報表,故以 dcc.Store() 存取 Submit 前的財報資料。


@app.callback(
    [Output('previous_fn_data', 'data')], # dcc.Store 要存的資料
    [Input('fn_report', 'data')] # 財務報表的資料
)

def save_data(data):

    return [data]

如需專案完整程式碼,請參閱文章最後的 Github 連結。


 

開發過程遇到的問題


1. 財報 (Data Table) 編輯後無法自動重整 / 重新運算


原本情境是整份專案只呈現一個表格在 <最下方 - 財務報表>。裡面直接讓使用者編輯欄位內容 (eg. 貨幣、匯率),但後來沒辦法使用同一個 Data Table 作為輸出 (Output) 與輸入 (Input) 故作罷,改以兩個 Data Table 呈現,並把需要讓使用者調整的內容放至 <中間層 - 客戶收費設定>,將其表格設定為可編輯。

 

2. 使用者第一次提交(Submit)後,再重設篩選條件時(尚未第二次提交),原財報會跳回空表格


因 <最下方 - 財務報表> 預設開啟頁面時不載入資料(給 empty dataframe)但這也導致即便使用者曾經提交過,只要又有參數變動,財務表格就又會跳回空表格。後來是藉由 dcc.Store() 規避了這個問題。


原本修正前的流程(使用 dcc.Store 之前):

步驟

行為

實際結果

預期結果

1

打開 Dash 頁面

財報資料為空

財報資料為空

2

選擇日期範圍(或調整任一篩選器)

同上

同上

3

點擊提交(Submit)

財報資料更新

財報資料更新

4

重新選擇日期範圍(或調整任一篩選器)

財報資料又跳空

財報資料維持不變(點擊Submit 前都應維持前次結果)


修正後流程:

步驟

行為

實際結果

預期結果

1

打開 Dash 頁面

財報資料為空

財報資料為空

2

選擇日期範圍(或調整任一篩選器)

同上

同上

3

點擊提交(Submit)

財報資料更新

財報資料更新

4

重新選擇日期範圍(或調整任一篩選器)

財報資料維持不變

財報資料維持不變(點擊Submit 前都應維持前次結果)

5

點擊提交(Submit)

財報資料更新

財報資料更新


➡️ 更多關於 dcc.Store 的設定可參考此處

 

3. 按鈕的 n-clicks 參數 / 條件設定


Button object 的 property 為 n-clicks,表示該按鈕被點擊的次數。


原本 <最下方 - 財務報表> 的觸發是 Sumbit 按鈕的 n-clicks > 0 (只要被點擊過)且其他 Input 不為 None 即觸發;但此行為會導致提交一次後只要再任意調整其他篩選器,都可在不點擊 Submit 的狀況繼續 refresh 財報內容(因第一次點擊後 n-clicks 就已經符合條件 > 0)


所以原本還要為此設立一個 Callback 是將 n-clicks 歸零的(現在想來真的挺怪)


後來看到 Community 更聰明的做法:


直接去判斷最近一次的頁面行為 (callback_context.triggered) 以取代上面這種複雜做法。故只需要判定「最近一次的頁面行為是否為點擊 Submit」即可,就不用再去判斷點擊次數是否 > 0。


@app.callback(
    [Output('fn_report', 'data')], # 財報資料
    [Input('submit', 'n_clicks'), # submit
     Input('date_picker_range', 'start_date'), # 日期選單 - 起始日
     Input('date_picker_range', 'end_date'), # 日期選單 - 終止日
     Input('previous_fn_data', 'data'), # 前次資料 - 前面提到的 dcc.Store
     ...
    ])
    
def get_data(submit, ...):
    
    changed_id = [action['prop_id'] for action in callback_context.triggered][0]
    
    if ('submit' in changed_id):
    
    ...

➡️ 更多 callback_context 應用可參考此處


 

這次文章主要都是記錄自己一路摸 Dash 的心路歷程,希望自己在未來開發能少踩點洞(汗


最後,若有任何問題也歡迎交流!也很歡迎各路大神提出任何可以優化的地方


若需要專案完整程式碼,可至我的 Github



bottom of page