今天我們來說明如何將gt
表格呈現於Panel app中,內容參考自官方Solar Zenith Angles範例及Panel主要維護者之一的Marc Skov Madsen在Holoviz論壇中所給出的範例(註1)。
Panel是一個類似於Streamlit或Gradio的web app框架,因為其內部使用Param來作為互動操作的核心,學習起來雖然有一定的難度,但是個人覺得其程式碼相對容易維護。此外,Panel與gt
的搭配也比一般框架來的簡單,所以想在今天的內容分享給大家。
今天的內容將部署於Py.Cafe,是一個使用Pyodide為技術的Python app部署平台,讓我們可以在瀏覽器中直接執行Python。由於Panel可以使用Pyodide執行,所以是Py.Cafe
平台推薦的選擇之一。此外,因為Polars尚無法於Py.Cafe
上安裝,所以我們會使用Pandas來整理Dataframe。
今天的內容將分為四大部份:
程式碼將存在main_static.py及main_dynamic檔中,其中動態表格成果可以於Py.Cafe中觀看。
此外需留意幾乎所有的Panel app都會需要加上一行pn.extension(),其功用為呼叫所需的JavaScript程式或套件。
原先範例中是使用Polars整理Dataframe:
sza_pivot = (
pl.from_pandas(sza)
.filter((pl.col("latitude") == "20") & (pl.col("tst") <= "1200"))
.select(pl.col("*").exclude("latitude"))
.drop_nulls()
.pivot(values="sza", index="month", on="tst", sort_columns=True)
)
我們使用Pandas改寫如下:
import panel as pn
from great_tables import GT, html
from great_tables.data import sza
@pn.cache
def get_sza_pivot():
return (
sza.assign(tst_int=lambda df_: df_.tst.astype(int))
.query("latitude == '20' and tst_int <= 1200")
.drop(["latitude", "tst_int"], axis=1)
.dropna()
.pivot(values=["sza"], index=["month"], columns=["tst"])
.droplevel(0, axis=1)
.reindex(
[
"jan",
"feb",
"mar",
"apr",
"may",
"jun",
"jul",
"aug",
"sep",
"oct",
"nov",
"dec",
],
axis=0,
)
.reset_index(names="month")
)
我們將整理DataFrame的邏輯寫入在get_sza_pivot()
,並使用pn.cache()
裝飾在該函數上。
眼尖的您可能發現,今天的例子不僅是快取一個返回DataFrame的函數,我們也將整理的步驟加入其中。原因是如果DataFrame的整理邏輯不會改變的話,一起擺進快取內也是個不錯的選擇。
DataFrame整理的步驟如下:
int
而來(註2)。axis=1
即是指定刪除欄位對象為列(axis=0
則是指定刪除欄位對象為行)。DataFrame.pivot()
多出來的階層,其中axis=1
即是指定刪除階層對象為列(axis=0
則是指定刪除階層對象為行)。get_table()
函數建立get_table()
,將範例中的製表程式碼存於其內,並使用get_sza_pivot()
的回傳值作為被GT
所包裹的DataFrame:
def get_table() -> GT:
return (
GT(get_sza_pivot(), rowname_col="month")
.data_color(
domain=[90, 0],
palette=["rebeccapurple", "white", "orange"],
na_color="white",
)
.tab_header(
title="Solar Zenith Angles from 05:30 to 12:00",
subtitle=html("Average monthly values at latitude of 20°N."),
)
.sub_missing(missing_text="")
)
FastListTemplate
呈現表格這裡我們使用了Panel的FastListTemplate,其內部已經預設好許多樣式,且可通過參數快速調整。而我們所要做的只是需要將get_table()
的回傳值main_content
,指定給main
參數。最後呼叫FastListTemplate.servable()來告知Panel,這將是我們想要Panel渲染的最終樣式。
# main_static.py
main_content = get_table()
pn.template.FastListTemplate(
site="Panel",
title="Great Tables",
main=[main_content],
main_layout=None,
accent="#70409f",
).servable()
現在我們可以於命令列中執行下列指令:
panel serve main_static.py
接著打開瀏覽器前往預設網址,如http://127.0.0.1:5006/main_static,就可以看見我們於Panel中建立的靜態表格:
接下來我們想要將靜態表格修改為動態表格,目標是希望透過新增兩個widget來選擇顏色,而表格的背景漸層顏色能做出相對應的變化。
get_table()
函數修改get_table()
函數,使其接受color1
及color2
兩個參數,並將這兩個參數置入GT.data_color()
的palette
參數中。
# main_dynamic.py
def get_table(color1: str, color2: str) -> GT:
return (
GT(get_sza_pivot(), rowname_col="month")
.data_color(
domain=[90, 0],
palette=[color1, "white", color2],
na_color="white",
)
.tab_header(
title="Solar Zenith Angles from 05:30 to 12:00",
subtitle=html("Average monthly values at latitude of 20°N."),
)
.sub_missing(missing_text="")
)
使用pn.widgets.ColorPicker來做為兩種顏色的選擇器,並使用Pn.Row將兩個ColorPicker
包裹起來,以及使用pn.layout.HSpacer來加上一些水平空白。
# main_dynamic.py
color1 = pn.widgets.ColorPicker(name="Color Picker1", value="#663399")
color2 = pn.widgets.ColorPicker(name="Color Picker2", value="#FFA500")
colors = pn.Row(
pn.layout.HSpacer(),
color1,
pn.layout.HSpacer(),
color2,
pn.layout.HSpacer(),
)
Panel提供許多可用來排版的widget,其名字都非常直觀,如Pn.Row
就是一個橫向的排版widget,而pn.layout.HSpacer
就是施加水平空白的widget。
pn.bind()
連接表格與顏色選擇器使用pn.bind()來將get_table()
函數與所需傳入的顏色連結起來(註3)。
# main_dynamic.py
bind_table = pn.bind(get_table, color1, color2)
table = pn.panel(bind_table, align="center")
main_content = pn.Column(colors, table)
pn.bind()
就像是Panel版本的functools.partial(),可以連結一個callable與需要變動的widget。當widget進行變動時,Panel將會負責傳遞其變動後的值給所綁定的callable。一旦綁定完成後,您可以將此綁定結果,也視為是一種widget。
接著使用pn.panel來包裹bind_table
,並設定align
參數為「"center"」。
再來使用pn.Column將colors
與table
依垂直方向排版,並命名為main_content
。
FastListTemplate
呈現表格最後一樣使用FastListTemplate
,並指定main
參數為main_content
後,呼叫FastListTemplate.servable()
來渲染成果。
pn.template.FastListTemplate(
site="Panel",
title="Great Tables",
main=[main_content],
main_layout=None,
accent="#70409f",
).servable()
再次於命令列中執行下列指令:
panel serve main_dynamic.py
接著打開瀏覽器前往預設網址,如http://127.0.0.1:5006/main_dynamic,就可以看見我們於Panel中建立的動態表格:
註1:今日改寫內容已事先取得Marc同意。
註2:其實這裡如果不新增「"tst_int"」欄(int
型別),而是針對「"tst"」欄(str
型別),使用DataFrame.query("latitude == '20' and tst_int <= '1200'")
也是可以。但我個人不是很喜歡這種依賴str
型別來比較大小的寫法,比較喜歡先轉換為數字之後再來比較大小,邏輯會比較清楚。
註3:pn.bind()
不是唯一的連結方式。如果您對其它方式有興趣,可以參考小弟於官方文件中改寫的範例,會說明如何使用更有效率的param.rx()來連結。
本日所有程式碼可參考gt-panel repo。