2025年6月12日 星期四

Python 學習筆記 : 用 Bokeh 繪製互動式圖表 (三) 長條圖

本篇旨在測試 Bokeh 的長條圖繪製方法. 

本系列之前文章參考 :



Bokeh 繪製長條圖的 Figure 物件方法有四個 : 


方法名稱 說明
vbar() 繪製垂直長條圖, 每個長條代表一個 x 類別,使用 top 設定高度
hbar() 繪製水平長條圖, 每個長條對應一個 y 類別,使用 right 設定長度
vbar_stack() 繪製堆疊式垂直長條圖, 以 stackers 指定多個欄位搭配 ColumnDataSource
hbar_stack() 繪製堆疊式水平長條圖, 與 vbar_stack() 類似,但方向為水平


首先來測試垂直長條圖方法 vbar(), 其參數結構如下 :

fig.vbar(x, top, *, width=0.9, bottom=0, color='blue', alpha=1.0, line_color=None, legend_label=None, **kwargs)

參數說明如下表 : 


fig.vbar() 參數 說明
x 指定長條的 x 軸位置(通常為分類值)
top 長條的高度(y 軸數值)
width 長條的寬度(占該分類寬度比率 0~1, 預設為 1)
bottom 長條的底部 y 座標(預設為 0)
fill_color 長條內部填滿的顏色,例如 "blue"、"#FF0000"、或 "rgba(255, 0, 0, 0.5)"
line_color 長條邊框顏色
line_width 邊框寬度(以像素為單位, 預設 1px)
legend_label 圖例標籤,若要在圖上加入圖例需設此參數
alpha 直條透明度 (0~1),0 表示完全透明,1 表示不透明


例如 : 


測試 1 : 預設的垂直長條圖 [看原始碼]   

# bokeh-bar-test-1.py
from bokeh.plotting import figure, show

x=[1, 2, 3, 4, 5]  # x 軸資料
y=[120000, 135000, 99000, 150000, 170000]  # y 軸資料
# 建立 figure 物件
fig=figure(width=400, height=300, title='Bokeh 長條圖')
# 繪製長條圖
fig.vbar(x, top=y)  
show(fig)  # 顯示圖表

此例 x 與 y 軸皆為數值資料 (top 參數要指定為 y 軸資料), 結果如下 : 




可見預設情況長條會占滿其分配到的全部寬度, 因此每一條都黏在一起, 這可以利用 width 參數設定 (0~1, 預設 1) : 


測試 2 : 設定長條圖的 width, alpha 與 color 參數 [看原始碼]   

# bokeh-bar-test-2.py
from bokeh.plotting import figure, show

x=[1, 2, 3, 4, 5]  # x 軸資料
y=[120000, 135000, 99000, 150000, 170000]  # y 軸資料
# 建立 figure 物件
fig=figure(width=400, height=300, title='Bokeh 長條圖')
# 繪製長條圖
fig.vbar(x, top=y, width=0.5, color='red', alpha=0.5)   
show(fig)  # 顯示圖表

此例設定長條寬度占該分類寬度之 50%, 底色+邊框顏色為 red, 填色透明度為 0.5, 結果如下 : 




可見 0.5 的透明度讓顏色變成近乎粉紅. 也可以用 line_with 設定邊框寬度 (預設為 0), 然後用 line_color 設定框線顏色, 用 fill_color 設定直條填色 : 


測試 3 : 設定長條圖的 line_width, line_color 與 fill_color 參數 (1) [看原始碼] 

# bokeh-bar-test-3.py
from bokeh.plotting import figure, show

x=[1, 2, 3, 4, 5]  # x 軸資料
y=[120000, 135000, 99000, 150000, 170000]  # y 軸資料
# 建立 figure 物件
fig=figure(width=400, height=300, title='Bokeh 長條圖')
# 繪製長條圖
fig.vbar(
    x,
    top=y,
    width=0.5,
    line_width=3,
    line_color='#0000ff',
    fill_color='yellow',
    alpha=0.5
    )  
show(fig)  # 顯示圖表

結果如下 : 




注意, alpha 會影響整個直條 (含邊框與填色), 如果只想設定填色透明度, fill_color 可以改為 RGBA 格式 "rgba(255, 255, 0, 0.5)", 例如 : 


測試 4 : 設定長條圖的 line_width, line_color 與 fill_color 參數 (2) [看原始碼] 

# bokeh-bar-test-4.py
from bokeh.plotting import figure, show

x=[1, 2, 3, 4, 5]  # x 軸資料
y=[120000, 135000, 99000, 150000, 170000]  # y 軸資料
# 建立 figure 物件
fig=figure(width=400, height=300, title='Bokeh 長條圖')
# 繪製長條圖
fig.vbar(
    x,
    top=y,
    width=0.5,
    line_width=3,
    line_color='#0000ff',
    fill_color='rgba(255, 255, 0, 0.5)'
    )  
show(fig)  # 顯示圖表

此例拿掉 alpha, 將 fill_color 改成 'rgba(255, 255, 0, 0.5)', 這樣邊框預設的藍色就不會受到 alpha 影響了, 結果如下 : 





下面範例是測試圖例標籤參數 legend_lebel 與其位置設定 :


測試 5 : 設定圖例標籤 legend_label 與圖例位置 [看原始碼] 

# bokeh-bar-test-5.py
from bokeh.plotting import figure, show

x=[1, 2, 3, 4, 5]  # x 軸資料
y=[120000, 135000, 99000, 150000, 170000]  # y 軸資料
# 建立 figure 物件
fig=figure(width=400, height=300, title='Bokeh 長條圖')
# 繪製長條圖
fig.vbar(x, top=y, width=0.5, legend_label='今年前五月月營收')
fig.legend[0].location='top_left'  # 設定圖例位置為左上
show(fig)  # 顯示圖表

注意, 用 Legend 物件的 location 屬性設定圖例位置一定要在呼叫 fig.vbar() 之後, 因為在這之前 Legend 物件還不存在. 結果如下 : 




可見圖例標籤已經放在左上角了 (預設為右上角, 這會蓋住長條圖). 

在上面的範例中, 由於 y 軸資料數字較大, Bokeh 會自動轉成科學記號來呈現, 如果想要以原來的整數表示, 關閉自動轉換機制, 只要將 fig.yaxis.formatter.use_scientific 設為 False 即可, 例如 :


測試 6 : 關閉 Y 軸的科學記號自動轉換 [看原始碼] 

# bokeh-bar-test-6.py
from bokeh.plotting import figure, show

x=[1, 2, 3, 4, 5]  # x 軸資料
y=[120000, 135000, 99000, 150000, 170000]  # y 軸資料
# 建立 figure 物件
fig=figure(width=400, height=300, title='Bokeh 長條圖')
fig.yaxis.formatter.use_scientific=False  # y 軸為整數
# 繪製長條圖
fig.vbar(x, top=y, width=0.5, color='cyan', line_color='blue')  
show(fig)  # 顯示圖表

結果如下 :




可見 Y 軸刻度標籤回復整數形式了. 

但如果要對 Y 軸刻度標籤進行格式化, 例如讓整數顯示千位逗號, 那就需要用到 bokeh.models 模組中的 NumeralTickFormatter 類別來設定 Y 軸軸物件 fig.yaxis 的 formatter 屬性了, 呼叫其建構式 NumeralTickFormatter() 並傳入格式化字串 (例如 "0,0" 表示整數) 會傳回一個 NumeralTickFormatter 物件, 將其設定給軸物件 fig.yaxis 的 formatter 屬性即可讓 Y 軸刻度標籤顯示指定格式了. 可傳入 NumeralTickFormatter() 的常用格式化字串如下表 :


常用格式化字串 說明
"0,0" 整數加千位分隔符,例如 10000 會顯示為 10,000
"0,0.0" 千位分隔符並保留一位小數,例如 10000.5 顯示為 10,000.5
"0%" 百分比格式,自動乘以 100,例如 0.5 顯示為 50%
"0.00%" 百分比格式,保留兩位小數,例如 0.5 顯示為 50.00%
"$0,0.00" 貨幣格式,有千位分隔與兩位小數,例如 1234.56 顯示為 $1,234.56
"+0,0" 顯示正負號的整數,例如 1000 顯示為 +1,000
"0.00a" 縮寫數值(k、M、B 等),例如 12300 顯示為 12.30k
"0.0b" 以位元組單位顯示數值,例如 1048576 顯示為 1.0MB
"0.000e+0" 科學記號表示法,例如 1230 顯示為 1.230e+3
"0o" 加上序數後綴,例如 1 顯示為 1st,2 顯示為 2nd


例如 :


測試 7 : 設定 Y 軸格式化刻度標籤 [看原始碼] 

# bokeh-bar-test-7.py
from bokeh.plotting import figure, show
from bokeh.models import NumeralTickFormatter   

x=[1, 2, 3, 4, 5]  # x 軸資料
y=[120000, 135000, 99000, 150000, 170000]  # y 軸資料
# 建立 figure 物件
fig=figure(width=400, height=300, title='Bokeh 長條圖')
fig.yaxis.formatter=NumeralTickFormatter(format='0,0')  # y 軸為帶千位逗號整數
# 繪製長條圖
fig.vbar(x, top=y, width=0.5, color='cyan', line_color='blue')  
show(fig)  # 顯示圖表

結果如下 :




可見 Y 軸刻度的標籤都以整數而非科學記號表示了. 

上面範例所用的 X 軸資料均為數值, 如果直接改為類別資料, 例如 :

x=['一月', '二月', '三月', '四月', '五月']

Bokeh 並不知道 x 軸是類別資料, 它仍會以數值來處理類別資料, 執行時雖然不會出現錯誤, 但繪製結果會是空白圖片. 解決之道是要在呼叫 figure() 時傳入 x_range 參數指定 x 軸為類別資料 x_range=x, 例如 : 


測試 8 : 呼叫 figure() 時用 x_range 參數設定 x 軸為類別資料 [看原始碼] 

# bokeh-bar-test-8.py
from bokeh.plotting import figure, show

x=['一月', '二月', '三月', '四月', '五月']  # x 軸資料
y=[120000, 135000, 99000, 150000, 170000]  # y 軸資料
# 建立 figure 物件
fig=figure(x_range=x, width=400, height=300, title='Bokeh 長條圖')
# 繪製長條圖
fig.vbar(x, top=y, width=0.5, color='orange', line_color='gray')  
show(fig)  # 顯示圖表

結果如下 :




可見 x 軸的刻度標籤已經顯示 "一月", "二月" 等字串了. 

上面的範例均使用串列做為 X, Y 軸的資料來源, 其實 Bokeh 的 bokeh.models 模組裡面有一個 ColumnDataSource 類別是專門為了處理資料來源而設計的, 與單純使用串列做為資料來源相比, ColumnDataSource 有如下之好處 :
  • 支援互動與資料綁定 : 
    可讓 Bokeh 元件與圖表綁定相同資料來源, 同步更新資料.
  • 支援資料更新與動態重繪 : 
    程式執行中若動態更新 ColumnDataSource 的資料, 圖表會自動重新繪製.
  • 多圖表資料共享 : 
    多圖表使用 ColumnDataSource 共享同一批資料可以使它們同步並節省記憶體.
  • 整合更進階的轉換器 (transformers) 函式 : 
    在 bokeh.transform 模組裡面有許多轉換器函式例如 dodge(), jitter(), factor_cmap() 等都需要搭配 ColumnDataSource 才能使用. 
呼叫建構式 ColumnDataSource() 並傳入一個字典或 DataFrame 會傳回一個 ColumnDataSource 物件, 然後在呼叫 Figure 物件的繪圖方法例如 line(), scatter() 或 vbar() 時將此物件傳給 source 參數即可依據此資料來源繪圖, 例如 : 


測試 9 : 使用 ColumnDataSource 物件做為資料來源 (字典) [看原始碼] 

# bokeh-bar-test-9.py
from bokeh.plotting import figure, show
from bokeh.models import ColumnDataSource

# 原始資料
x=[1, 2, 3, 4, 5]
y=[120000, 135000, 99000, 150000, 170000]
# 建立 ColumnDataSource
source=ColumnDataSource(data=dict(x=x, y=y))
# 建立 figure 物件
fig=figure(width=400, height=300, title='Bokeh 長條圖')
# 繪製長條圖,欄位名稱須用字串對應到 source 中的欄位
fig.vbar(
    x='x',  # 對應到 source 的 'x' 鍵 (注意是字串)
    top='y',  # 對應到 source 的 'y' 鍵 (注意是字串)
    width=0.5,
    color='orange',
    line_color='gray',
    source=source  # 指定資料來源為 ColumnDataSource 物件
    )
# 顯示圖表
show(fig)

此例將原始的 x, y 軸串列資料來源用 dict() 打包成 'x', 'y' 鍵的字典後傳給 ColumnDataSource() 的 data 參數建立 ColumnDataSource 物件, 並於呼叫 fig.vbar() 時將其傳給 source 參數. 注意此處 vbar() 的 x 與 top 參數必須是 source 物件中的 'x' 與 'y' 鍵, 不是 x 與 y. 結果如下 :




這與直接使用串列做為資料來源之結果是一樣的. 

Bokeh 的 ColumnDataSource 與 Pandas 高度整合, 也可以直接傳入 DataFrame, 例如 :


測試 10 : 使用 ColumnDataSource 物件做為資料來源 (DataFrame) [看原始碼] 

# bokeh-bar-test-10.py
from bokeh.plotting import figure, show
from bokeh.models import ColumnDataSource
import pandas as pd

# 原始資料
df=pd.DataFrame({
    'x': ['一月', '二月', '三月', '四月', '五月'],
    'y': [120000, 135000, 99000, 150000, 170000]
    })
# 建立 ColumnDataSource
source=ColumnDataSource(df)  # 傳入 DataFrame 
# 建立 figure 物件
fig=figure(x_range=df['x'], width=400, height=300, title='Bokeh 長條圖')
fig.yaxis.formatter.use_scientific=False
# 繪製長條圖,欄位名稱須用字串對應到 source 中的欄位
fig.vbar(
    x='x',  # 對應到 source 的 'x' 鍵 (注意是字串)
    top='y',  # 對應到 source 的 'y' 鍵 (注意是字串)
    width=0.5,
    color='orange',
    line_color='gray',
    source=source  # 指定資料來源為 ColumnDataSource 物件
    )
# 顯示圖表
show(fig)

此例改用分類資料 (中文月份字串) 做為 x 軸, 需要注意的是 figure() 要傳入 x_range 參數且指定 df 的 'x' 鍵 Series, 結果如下 : 




ColumnDataSource 在繪製多個 Y 軸資料時 (群組式長條圖) 很有用, 但要在 X 軸的一個刻度上顯示多個長條還需要用到 bokeh.transform 模組裡面的 dodge() 函式, 其功能類似在 Matplotlib 繪製群組長條圖的做法, 用來在刻度左右兩側的偏移位置分別畫各類別的長條. 

dodge() 的參數結構如下 :

dodge(field_name, value, range)

參數說明如下表 :


dodge() 參數 說明
field 欄位名稱 (字串),代表分類資料所在欄位,例如 "x"。
value 數值型的偏移量,用來決定每組資料應在分類軸上左右偏移多少,例如 ±0.2。
range 分類軸的範圍 (通常為 fig.x_rangeFactorRange),用於正確計算偏移。


dodge() 會傳回一個 Dodge 物件, 這是一個 Bokeh 中的 transform 轉換器, 用來告訴 Bokeh 如何處理資料座標偏移, Bokeh 會在繪圖時根據 Dodge 物件和 ColumnDataSource 物件的資料自行計算長條的實際繪製位置, 例如 : 


測試 11 : 利用 bokeh.transform.dodge() 繪製群組式長條圖 [看原始碼] 

# bokeh-bar-test-11.py
from bokeh.plotting import figure, show
from bokeh.transform import dodge   
from bokeh.models import ColumnDataSource

x=['一月', '二月', '三月', '四月', '五月']  # x 軸資料
y1=[120000, 135000, 99000, 150000, 170000]  # y 軸資料
y2=[100000, 125000, 87000, 140000, 160000]
# 建立 ColumnDataSource
source=ColumnDataSource(data=dict(x=x, y1=y1, y2=y2))
# 建立 figure 物件
fig=figure(x_range=x, width=500, height=400, title='Bokeh 群組長條圖')
fig.yaxis.formatter.use_scientific=False  # y 軸為整數
# 繪製第一個長條圖
fig.vbar(
    x=dodge('x', -0.2, range=fig.x_range),   # 往左偏移 20%
    top='y1',
    width=0.4,
    color='navy',
    legend_label='今年營收',
    source=source
    )
# 繪製第二個長條圖
fig.vbar(
    x=dodge('x', 0.2, range=fig.x_range),   # 往右偏移 20%
    top='y2',
    width=0.4,
    color='cyan',
    legend_label='去年營收',
    source=source
    )
fig.legend.location='top_left'
show(fig)  # 顯示圖表

此例呼叫了兩次 fig.vbar() 分別繪製今年與去年的營收長條, 關鍵在傳入的 x 參數是由 dodge() 向左向右偏移 20% 後調整過的座標位置, 結果如下 :




此例將圖片尺寸放大為 500x400 以避免圖例遮蓋了長條. 

沒有留言 :