顯示具有 Bokeh 標籤的文章。 顯示所有文章
顯示具有 Bokeh 標籤的文章。 顯示所有文章

2026年5月3日 星期日

Python 學習筆記 : 用 bokeh 繪製 K 線圖 (二)

Bokeh 的優點是高自由度與高效能, 但其 API 較低階, 故並未內建像 Plotly 的 CandleStick 那樣的類別, 本篇旨在為 Bokey 山寨一個 CandleStick 來簡化 K 線圖的繪製. 為了讓程式碼不至於過度複雜, 以下測試不處理非交易日 K 棒空缺問題. 

本系列全部測試文章索引參考 :



4. 用封裝的類別繪製 K 線圖 :   

下面範例改寫自前一篇測試的 bokeh_candlestick_3.py, 將繪製 K 線圖的邏輯封裝在自訂的 BokehChart 類別裡, 只要呼叫其建構式並傳入 title/width/height 參數建立物件, 然後在主程式中呼叫 add_candlestick(df) 即可繪製 K 線圖, 程式碼如下 : 

# bokeh_candlestick_class_1.py
import yfinance as yf
import pandas as pd
from bokeh.plotting import figure, show
from bokeh.models import ColumnDataSource, HoverTool, CrosshairTool, Span

class BokehChart:
    """封裝 Bokeh K線圖與技術分析工具的繪圖類別"""
    def __init__(self, title="K線圖", width=800, height=400):
        # 建立並初始化畫布
        self.fig=figure(
            x_axis_type='datetime', 
            title=title, 
            width=width,
            height=height,
            tools='xpan, xwheel_zoom, box_zoom, reset, save'
            )
        self.fig.grid.grid_line_alpha=0.3
        
        # 初始化虛線十字游標
        self._add_crosshair()

    def _add_crosshair(self):
        """內部方法:加入虛線十字游標"""
        w_span=Span(dimension="width", line_dash="dashed", line_color="gray", line_alpha=0.6, line_width=1)
        h_span=Span(dimension="height", line_dash="dashed", line_color="gray", line_alpha=0.6, line_width=1)
        self.fig.add_tools(CrosshairTool(overlay=[w_span, h_span]))

    def add_candlestick(self, df, date_col='Date', open_col='Open', high_col='High', low_col='Low', close_col='Close', vol_col='Volume'):
        """對外介面:加入 K 線圖與懸停工具"""
        # 複製資料以避免修改到原始 DataFrame
        data=df.copy()
        
        # 定義漲跌顏色 (紅漲綠跌)
        data['color']=['#ff0000' if c >= o else '#00aa00' for o, c in zip(data[open_col], data[close_col])]
        
        # 建立資料來源
        source=ColumnDataSource(data)
        
        # 繪製上下影線
        self.fig.segment(date_col, high_col, date_col, low_col, color="black", source=source)
        
        # 繪製 K 棒 (12 小時寬度)
        width_ms=12 * 60 * 60 * 1000
        self.fig.vbar(date_col, width_ms, open_col, close_col, 
                      fill_color='color', line_color='black', source=source)
        
        # 加入互動式懸停工具 (HoverTool)
        hover=HoverTool(
            tooltips=[
                ("日期", f"@{date_col}{{%F}}"),
                ("開盤", f"@{open_col}{{0.00}}"),
                ("收盤", f"@{close_col}{{0.00}}"),
                ("最高", f"@{high_col}{{0.00}}"),
                ("最低", f"@{low_col}{{0.00}}"),
                ("成交量", f"@{vol_col}{{0,0}}")
                ],
            formatters={f'@{date_col}': 'datetime'}
            )
        self.fig.add_tools(hover)

    def show(self):
        """顯示圖表"""
        show(self.fig)

# ==========================================
# 主程式:使用類別繪圖 (如同 Plotly 般簡潔)
# ==========================================
if __name__ == "__main__":
    # 1. 下載真實 OHLCV 資料
    df=yf.download('0050.tw', start='2026-03-01', end='2026-04-30', auto_adjust=True)
    df.columns=df.columns.map(lambda x: x[0])
    df=df.reset_index()

    # 2. 實例化畫布並繪圖
    chart=BokehChart(title='0050.TW 台灣50 ETF K線圖 (2026-03 ~ 2026-04)', width=800, height=400)
    chart.add_candlestick(df)
    
    # 3. 顯示圖表
    chart.show()

結果如下 :




5. 用封裝的類別繪製 K 線圖 + 成交量副圖 :   

如果要同時繪製 K 線圖與成交量副圖, 則須匯入 bokeh.layouts.column() 函式進行垂直布局, 讓主圖 (fig_k) 與副圖能共用 X 軸並連動. 程式碼改寫如下 : 

# bokeh_candlestick_class_2.py
import yfinance as yf
import pandas as pd
from bokeh.plotting import figure, show
from bokeh.models import ColumnDataSource, HoverTool, CrosshairTool, Span, CustomJSTickFormatter
from bokeh.layouts import column  # 引入垂直佈局工具

class BokehChart:
    """封裝 Bokeh K線圖與成交量子圖的繪圖類別"""
    
    # 稍微調整初始化參數,允許分別設定主圖與副圖的高度
    def __init__(self, title="K線圖", width=800, height_k=400, height_v=150):
        # 1. 建立主圖 (K線圖)
        self.fig_k=figure(
            title=title, 
            width=width, height=height_k,
            tools='xpan, xwheel_zoom, box_zoom, reset, save'
            )
        self.fig_k.grid.grid_line_alpha=0.3
        self.fig_k.xaxis.visible=False  # 隱藏主圖的 X 軸,讓畫面更緊湊
        
        # 2. 建立副圖 (成交量圖)
        self.fig_v=figure(
            width=width, height=height_v,
            x_range=self.fig_k.x_range,  # 【關鍵連動】將副圖的 X 軸範圍綁定為主圖的 X 軸
            tools='xpan, xwheel_zoom, reset'
            )
        self.fig_v.grid.grid_line_alpha=0.3
        self.fig_v.y_range.start=0     # 確保成交量 Y 軸從 0 開始
        
        # 3. 組合為垂直佈局
        self.layout=column(self.fig_k, self.fig_v)
        
        # 4. 初始化虛線十字游標 (加在兩張圖上)
        self._add_crosshair(self.fig_k)
        self._add_crosshair(self.fig_v)

    def _add_crosshair(self, fig):
        """內部方法:為指定的畫布加入虛線十字游標"""
        w_span=Span(dimension="width", line_dash="dashed", line_color="gray", line_alpha=0.6, line_width=1)
        h_span=Span(dimension="height", line_dash="dashed", line_color="gray", line_alpha=0.6, line_width=1)
        fig.add_tools(CrosshairTool(overlay=[w_span, h_span]))

    def add_candlestick(self, df, date_col='Date', open_col='Open', high_col='High', low_col='Low', close_col='Close', vol_col='Volume'):
        """對外介面:處理資料並繪製 K 線與成交量"""
        data=df.copy()
        
        # 建立連續整數序列 (消除假日空缺)
        data['seq']=range(len(data))
        
        # 定義漲跌顏色
        data['color']=['#ff0000' if c >= o else '#00aa00' for o, c in zip(data[open_col], data[close_col])]
        
        # 用 Python 預先處理好 Bokeh 風格的日期字串
        date_map={}
        prev_year=None
        for i, d in enumerate(data[date_col]):
            label=f"{d.strftime('%b')} {d.day}"
            if i == 0 or d.year != prev_year:
                label += f"\n{d.year}"
            date_map[str(i)]=label
            prev_year=d.year
            
        source=ColumnDataSource(data)
        width_val=0.8  # 因為 X 軸變成了連續整數,寬度直接用比例 0.8
        
        # --- 繪製主圖 (K線) ---
        self.fig_k.segment('seq', high_col, 'seq', low_col, color="black", source=source)
        self.fig_k.vbar('seq', width_val, open_col, close_col, fill_color='color', line_color='black', source=source)
        
        # --- 繪製副圖 (成交量) ---
        # 使用 vbar,top 參數指定為成交量欄位
        self.fig_v.vbar('seq', width_val, top=vol_col, fill_color='color', line_color='black', source=source)
        
        # --- 設定副圖的 X 軸日期標籤 ---
        js_code="""
            var idx=Math.round(tick).toString();
            if (date_map[idx] !== undefined) {
                return date_map[idx];
            } else {
                return "";
            }
        """
        self.fig_v.xaxis.formatter=CustomJSTickFormatter(code=js_code, args={'date_map': date_map})
        self.fig_v.xaxis.major_label_orientation=0 
        self.fig_v.xaxis.major_label_standoff=15
        
        # --- 加入互動式懸停工具 ---
        hover=HoverTool(
            tooltips=[
                ("日期", f"@{date_col}{{%F}}"),
                ("開盤", f"@{open_col}{{0.00}}"),
                ("收盤", f"@{close_col}{{0.00}}"),
                ("最高", f"@{high_col}{{0.00}}"),
                ("最低", f"@{low_col}{{0.00}}"),
                ("成交量", f"@{vol_col}{{0,0}}")
                ],
            formatters={f'@{date_col}': 'datetime'},
            mode='vline'  # 【推薦】改為 vline 模式,滑鼠對齊同一個垂直線就會觸發
            )
        self.fig_k.add_tools(hover)
        self.fig_v.add_tools(hover)

    def show(self):
        """顯示圖表佈局"""
        show(self.layout)  # 【修改】現在是顯示整個 layout 而不是單一 fig

# ==========================================
# 主程式
# ==========================================
if __name__ == "__main__":
    # 1. 下載實盤 OHLCV 資料
    df=yf.download('0050.tw', start='2026-03-01', end='2026-04-30', auto_adjust=True)
    df.columns=df.columns.map(lambda x: x[0])
    df=df.reset_index()

    # 2. 建立畫布並繪圖
    chart=BokehChart(title='0050.TW 台灣50 ETF (含同步成交量)')
    chart.add_candlestick(df)
    
    # 3. 顯示圖表
    chart.show()

注意, 此例之 BokehChart 類別還封裝了處理非交易日空缺的邏輯, 這讓整個畫布上的 K 棒看起來是連續無空缺的, 結果如下 : 




6. 用封裝的類別繪製 K 線圖 + 成交量 + 技術指標副圖 : 

上面範例中的 BokehChart 類別把主圖與副圖邏輯都寫在類別裡, 每次要添加技術指標副圖都要去改類別並不符合軟工原則, 若要讓程式碼具有擴充性, 必須對架構做一次重構, 要建立一個 動態串列 self.figures = [] 的來儲存所有畫布, 並增加通用副圖方法, 例如 add_bar_subplot() (畫成交量, MACD柱狀圖) 與 add_line_subplot() (畫 RSI, 均線).

程式碼如下 : 

# bokeh_candlestick_class_3.py
import yfinance as yf
import pandas as pd
import pandas_ta as ta  # 
from bokeh.plotting import figure
from bokeh.io import show as show_bokeh # 避免名稱衝突
from bokeh.models import ColumnDataSource, HoverTool, CrosshairTool, Span, CustomJSTickFormatter
from bokeh.layouts import column

class BokehChart:
    """高擴充性 Bokeh 繪圖類別,支援動態疊加無限多個副圖"""
    
    def __init__(self, title="K線圖", width=800, main_height=400):
        self.width=width
        self.title=title
        self.main_height=main_height
        
        # 動態儲存所有畫布的清單 (主圖會在 index 0)
        self.figures=[]
        self.source=None
        self.date_map={}
        self.fig_k=None # 參照主圖,用來同步 x_range

    def _add_crosshair(self, fig):
        """內部方法:加入虛線十字游標"""
        w_span=Span(dimension="width", line_dash="dashed", line_color="gray", line_alpha=0.6, line_width=1)
        h_span=Span(dimension="height", line_dash="dashed", line_color="gray", line_alpha=0.6, line_width=1)
        fig.add_tools(CrosshairTool(overlay=[w_span, h_span]))

    def add_candlestick(self, df, date_col='Date', open_col='Open', high_col='High', low_col='Low', close_col='Close'):
        """【步驟 1】:初始化主圖與共用資料源"""
        data=df.copy()
        
        # 建立連續整數序列 (消除假日空缺)
        data['seq']=range(len(data))
        data['color']=['#ff0000' if c >= o else '#00aa00' for o, c in zip(data[open_col], data[close_col])]
        
        # 製作日期對照表
        self.date_map={}
        prev_year=None
        for i, d in enumerate(data[date_col]):
            label=f"{d.strftime('%b')} {d.day}"
            if i == 0 or d.year != prev_year:
                label += f"\n{d.year}"
            self.date_map[str(i)]=label
            prev_year=d.year
            
        # 建立全局共用的資料源 (此時 df 裡面已經包含外部算好的 RSI 等指標)
        self.source=ColumnDataSource(data)
        
        # 建立主畫布
        self.fig_k=figure(title=self.title, width=self.width, height=self.main_height, tools='xpan, xwheel_zoom, box_zoom, reset, save')
        self.fig_k.grid.grid_line_alpha=0.3
        
        # 繪製 K 線
        self.fig_k.segment('seq', high_col, 'seq', low_col, color="black", source=self.source)
        self.fig_k.vbar('seq', 0.8, open_col, close_col, fill_color='color', line_color='black', source=self.source)
        
        # 設定主圖 Hover
        hover=HoverTool(
            tooltips=[("日期", f"@{date_col}{{%F}}"), ("開盤", f"@{open_col}{{0.00}}"), ("收盤", f"@{close_col}{{0.00}}")],
            formatters={f'@{date_col}': 'datetime'}, mode='vline'
            )
        self.fig_k.add_tools(hover)
        self._add_crosshair(self.fig_k)
        
        # 加入畫布清單
        self.figures.append(self.fig_k)

    def add_bar_subplot(self, col_name, title="", color_col=None, color='blue', height=150):
        """通用介面:新增柱狀圖副圖 (例如:成交量、MACD柱)"""
        fig=figure(width=self.width, height=height, x_range=self.fig_k.x_range, tools='xpan, xwheel_zoom, reset')
        fig.grid.grid_line_alpha=0.3
        fig.y_range.start=0
        
        # 若有指定顏色欄位 (如成交量的紅綠色),則使用該欄位;否則使用單一顏色
        fill_c=color_col if color_col else color
        fig.vbar('seq', 0.8, top=col_name, fill_color=fill_c, line_color='black', source=self.source)
        
        # 專屬 Hover
        hover=HoverTool(tooltips=[("日期", "@Date{%F}"), (title or col_name, f"@{col_name}{{0,0}}")], formatters={'@Date': 'datetime'}, mode='vline')
        fig.add_tools(hover)
        self._add_crosshair(fig)
        self.figures.append(fig)

    def add_line_subplot(self, col_name, title="", color='blue', height=150, hlines=None):
        """通用介面:新增折線圖副圖 (例如:RSI、均線、KD)"""
        fig=figure(width=self.width, height=height, x_range=self.fig_k.x_range, tools='xpan, xwheel_zoom, reset')
        fig.grid.grid_line_alpha=0.3
        
        fig.line('seq', col_name, color=color, line_width=1.5, source=self.source)
        
        # 支援加入水平參考線 (例如 RSI 的 30, 70 超買超賣線)
        if hlines:
            for y_val in hlines:
                line=Span(location=y_val, dimension='width', line_color='gray', line_dash='dashed', line_alpha=0.5)
                fig.add_layout(line)

        # 專屬 Hover
        hover=HoverTool(tooltips=[("日期", "@Date{%F}"), (title or col_name, f"@{col_name}{{0.00}}")], formatters={'@Date': 'datetime'}, mode='vline')
        fig.add_tools(hover)
        self._add_crosshair(fig)
        self.figures.append(fig)

    def show(self):
        """渲染圖表:隱藏除最後一張圖外的 X 軸,並套用自訂日期標籤"""
        for i, fig in enumerate(self.figures):
            # 不是最後一張圖,隱藏 X 軸
            if i < len(self.figures) - 1:
                fig.xaxis.visible=False
            # 是最後一張圖,掛上 JavaScript 日期查表邏輯
            else:
                js_code="""
                    var idx=Math.round(tick).toString();
                    if (date_map[idx] !== undefined) {
                        return date_map[idx];
                        }
                    else {
                        return "";
                        }
                """
                fig.xaxis.formatter=CustomJSTickFormatter(code=js_code, args={'date_map': self.date_map})
                fig.xaxis.major_label_orientation=0
                fig.xaxis.major_label_standoff=15

        # 垂直疊加所有被加入的圖表
        layout=column(*self.figures)
        show_bokeh(layout)


# ==========================================
# 主程式:展現架構擴充性的時刻
# ==========================================
if __name__ == "__main__":
    # 1. 獲取資料
    df=yf.download('0050.tw', start='2025-10-01', end='2026-04-30', auto_adjust=True)
    df.columns=df.columns.map(lambda x: x[0])
    df=df.reset_index()

    # 2. 在外部計算所有技術指標 (類別完全不需要知道 RSI 怎麼算的)
    df['RSI']=ta.rsi(df['Close'], length=14)

    # 3. 開始繪圖 (如堆積木般直覺)
    chart=BokehChart(title='0050.TW 台灣50 ETF (整合 RSI 指標)')
    
    # 初始化主圖與資料源
    chart.add_candlestick(df)
    
    # 呼叫通用介面新增成交量 (使用 color 欄位決定紅綠)
    chart.add_bar_subplot('Volume', title='成交量', color_col='color', height=100)
    
    # 呼叫通用介面新增 RSI (自訂顏色,並加上 30/70 水平參考線)
    chart.add_line_subplot('RSI', title='RSI(14)', color='purple', height=150, hlines=[30, 70])
    
    # 顯示成果
    chart.show()

此例使用 pandas_ta 套件計算 RSI 指標值, 結果放進 df['RSI'] 欄位, 結果如下 : 




7. 獨立的 Bokeh K 線圖模組 : 

我們可以將上面範例的 BokehChart 類別寫在一個獨立的模組檔案例如 bokeh_chart.py 中, 再用 import 方式匯入 BokehChart 類別來繪製 K 線圖, 這樣主程式檔就會較簡潔了. 

模組檔案 bokeh_chart.py 內容如下 :

# bokeh_chart.py
import pandas as pd
from bokeh.plotting import figure
from bokeh.io import show as show_bokeh
from bokeh.models import ColumnDataSource, HoverTool, CrosshairTool, Span, CustomJSTickFormatter
from bokeh.layouts import column

class BokehChart:
    """高擴充性 Bokeh 繪圖類別,支援動態疊加無限多個副圖"""
    
    def __init__(self, title='K線圖', width=800, main_height=400):
        self.width=width
        self.title=title
        self.main_height=main_height
        self.figures=[]
        self.source=None
        self.date_map={}
        self.fig_k=None 

    def _add_crosshair(self, fig):
        """加入虛線十字游標"""
        w_span=Span(dimension='width', line_dash='dashed', line_color="gray", line_alpha=0.6, line_width=1)
        h_span=Span(dimension='height', line_dash='dashed', line_color="gray", line_alpha=0.6, line_width=1)
        fig.add_tools(CrosshairTool(overlay=[w_span, h_span]))

    def add_candlestick(self, df, date_col='Date', open_col='Open', high_col='High', low_col='Low', close_col='Close'):
        """初始化主圖與共用資料源"""
        data=df.copy()
        # 建立連續整數序列與漲跌顏色
        data['seq']=range(len(data))
        data['color']=['#ff0000' if c >= o else '#00aa00' for o, c in zip(data[open_col], data[close_col])]
        # 製作日期對照表
        self.date_map={}
        prev_year=None
        for i, d in enumerate(data[date_col]):
            label=f"{d.strftime('%b')} {d.day}"
            if i == 0 or d.year != prev_year:
                label += f"\n{d.year}"
            self.date_map[str(i)]=label
            prev_year=d.year
        self.source=ColumnDataSource(data)
        self.fig_k=figure(title=self.title, width=self.width, height=self.main_height, tools='xpan, xwheel_zoom, box_zoom, reset, save')
        self.fig_k.grid.grid_line_alpha=0.3
        self.fig_k.segment('seq', high_col, 'seq', low_col, color="black", source=self.source)
        self.fig_k.vbar('seq', 0.8, open_col, close_col, fill_color='color', line_color='black', source=self.source)
        hover=HoverTool(
            tooltips=[("日期", f"@{date_col}{{%F}}"), ("開盤", f"@{open_col}{{0.00}}"), ("收盤", f"@{close_col}{{0.00}}")],
            formatters={f'@{date_col}': 'datetime'}, mode='vline'
            )
        self.fig_k.add_tools(hover)
        self._add_crosshair(self.fig_k)
        self.figures.append(self.fig_k)

    def add_bar_subplot(self, col_name, title="", color_col=None, color='blue', height=150, on_fig=None, y_min_zero=False):
        """新增柱狀圖副圖 (支援傳入畫布疊加到現有畫布)"""
        # 如果有指定 on_fig,就用現成的畫布;否則建立新畫布
        if on_fig:
            fig=on_fig
        else:
            fig=figure(width=self.width, height=height, x_range=self.fig_k.x_range, tools='xpan, xwheel_zoom, reset')
            fig.grid.grid_line_alpha=0.3
            self.figures.append(fig) # 只有新畫布才需要加入清單
            self._add_crosshair(fig) # 只有新畫布才需要加十字線
        # 只有成交量這類數值絕對為正的圖表,才需要強制 Y 軸從 0 開始
        if y_min_zero:
            fig.y_range.start=0
        fill_c=color_col if color_col else color
        fig.vbar('seq', 0.8, top=col_name, fill_color=fill_c, line_color='black', source=self.source)
        # 專屬 Hover
        hover=HoverTool(tooltips=[("日期", "@Date{%F}"), (title or col_name, f"@{col_name}{{0.00}}")], formatters={'@Date': 'datetime'}, mode='vline')
        fig.add_tools(hover)
        return fig # 回傳畫布讓後續的線可以畫在它上面

    def add_line_subplot(self, col_name, title="", color='blue', height=150, hlines=None, on_fig=None):
        """新增折線圖副圖 (支援疊加到現有畫布)"""
        # 如果有指定 on_fig,就用現成的畫布;否則建立新畫布
        if on_fig:
            fig = on_fig
        else:
            fig = figure(width=self.width, height=height, x_range=self.fig_k.x_range, tools='xpan, xwheel_zoom, reset')
            fig.grid.grid_line_alpha = 0.3
            self.figures.append(fig)
            self._add_crosshair(fig)
        fig.line('seq', col_name, color=color, line_width=1.5, source=self.source)
        if hlines:
            for y_val in hlines:
                line = Span(location=y_val, dimension='width', line_color='gray', line_dash='dashed', line_alpha=0.5)
                fig.add_layout(line)
        hover = HoverTool(tooltips=[("日期", "@Date{%F}"), (title or col_name, f"@{col_name}{{0.00}}")], formatters={'@Date': 'datetime'}, mode='vline')
        fig.add_tools(hover)
        return fig # 回傳畫布讓後續的線可以畫在它上面

    def show(self):
        """渲染圖表"""
        for i, fig in enumerate(self.figures):
            if i < len(self.figures) - 1:
                fig.xaxis.visible=False
            else:
                js_code="""
                    var idx=Math.round(tick).toString();
                    if (date_map[idx] !== undefined) {
                        return date_map[idx];
                    } else {
                        return "";
                    }
                """
                fig.xaxis.formatter=CustomJSTickFormatter(code=js_code, args={'date_map': self.date_map})
                fig.xaxis.major_label_orientation=0
                fig.xaxis.major_label_standoff=15
        layout=column(*self.figures)
        show_bokeh(layout)

欲繪製 K 線圖+成交量+技術指標時, 主程式檔只要先從 bokeh_chart.py 匯入 BokehChart 類別並建立 BokehChart 物件後, 呼叫 add_candlestick() 繪製主圖 (K 線圖), 呼叫 add_bar_subplot() 或 add_line_subplot() 添加副圖 (成交量 & 技術指標), 下面範例添加了 RSI 與 MACD 副圖 :

# bokeh_candlestick_class_4.py
import yfinance as yf
import pandas as pd
import pandas_ta as ta
from bokeh_chart import BokehChart

if __name__ == "__main__":
    # 1. 獲取資料
    df=yf.download('0050.tw', start='2025-10-01', end='2026-04-30', auto_adjust=True)
    df.columns=df.columns.map(lambda x: x[0])
    df=df.reset_index()

    # 2. 計算技術指標
    # RSI
    df['RSI']=ta.rsi(df['Close'], length=14)
    # MACD (將計算結果合併回原本的 df)
    macd_df=ta.macd(df['Close'])
    df=pd.concat([df, macd_df], axis=1)
    # 計算 MACD 柱狀圖的顏色 (大於等於 0 紅色,小於 0 綠色)
    df['macd_color']=['#ff0000' if m >= 0 else '#00aa00' for m in df['MACDh_12_26_9']]

    # 3. 畫圖
    # 畫主圖 : K 線圖
    chart=BokehChart(title='0050.TW 台灣50 ETF (整合 MACD)')
    chart.add_candlestick(df)  
    # 畫成交量副圖 (加上 y_min_zero=True,避免長條圖懸空)
    chart.add_bar_subplot('Volume', title='成交量', color_col='color', height=100, y_min_zero=True)
    # ==========================================
    # 繪製 MACD 複合副圖
    # ==========================================
    # 第一步:先畫 MACD 柱狀圖,並用變數 macd_fig 把畫布接住
    macd_fig=chart.add_bar_subplot('MACDh_12_26_9', title='MACD柱', color_col='macd_color', height=150)
    # 第二步:把 MACD 線畫在 macd_fig 身上
    chart.add_line_subplot('MACD_12_26_9', title='MACD線', color='blue', on_fig=macd_fig)
    # 第三步:把 Signal 線也畫在 macd_fig 身上
    chart.add_line_subplot('MACDs_12_26_9', title='Signal線', color='orange', on_fig=macd_fig)
    # ==========================================

    chart.show()

注意, 此處 MACD 指標因為含有快線, 慢線, 與柱狀圖三個圖形, 通常都是疊在一個畫布上, 所以要分成三個步驟把三個圖層疊上去, 首先繪製柱狀圖, 然後將傳回的畫布物件 macd_fig 在後續繪製快線與慢線時傳給 on_fonfig 參數, 這樣三張圖就會疊在同一張畫布上了. 

結果如下 : 




可見將繪圖類別獨立出去後, 主程式結構就清爽易讀多了. 


Python 學習筆記 : 用 bokeh 繪製 K 線圖 (一)

Bokeh 不像 Plotly 那樣有內建專門繪製 K 線圖的 CandleStick 類別, 它需透過 bokeh.models 模組中的 Segment 物件 (用來繪製上下影線) 與 Vbar 物件 (用來繪製實體紅黑棒) 來達成, 方法雖然較低階, 但卻比 Plotly 擁有更高的自由度與效能, 尤其是要在瀏覽器中渲染數萬根 K 線時, Bokeh 的 WebGL 渲染模式通常比 Plotly 更為輕快, 且保持流暢的縮放和平移效果. 

本系列全部測試文章索引參考 :


首先匯入繪圖用的函式 figure() 與 show() : 

>>> from bokeh.plotting import figure, show     

然後從元件模組 bokeh.models 中匯入匯入用來繪製 K 棒的 ColumnDataSource 類別, 以及繪製互動效果的懸停工具類別 HoverTool : 
  
>>> from bokeh.models import ColumnDataSource, HoverTool  

接著匯入 Pandas 來處理 OHLC 資料 :

>>> import pandas as pd 
>>> data={
    'date': pd.to_datetime(['2026-05-01', '2026-05-02', '2026-05-03', '2026-05-04']),
    'open':  [100, 110, 105, 120],
    'high':  [115, 120, 110, 130],
    'low':   [95, 105, 100, 115],
    'close': [110, 105, 108, 125]
    }
>>> df=pd.DataFrame(data)   

在 df 中添加一個 color 欄位來記錄漲跌顏色 (紅漲綠跌) :

>>> df['color']=['#ff0000' if c >= o else '#00aa00' for o, c in zip(df.open, df.close)]   

呼叫 ColumnDataSource 類別的建構式 ColumnDataSource() 並傳入 df 建立 ColumnDataSource 物件 : 

>>> source=ColumnDataSource(df)   
>>> type(source)   
<class 'bokeh.models.sources.ColumnDataSource'>  

呼叫 figure() 函式建立畫布物件 : 

>>> fig=figure(  
    x_axis_type='datetime', 
    title='Bokeh K線圖', 
    width=800,
    height=400
    )

呼叫 segment() 函式繪製上下影線 :

>>> fig.segment('date', 'high', 'date', 'low', color='black', source=source)    
GlyphRenderer(id='p1138', ...)   

呼叫 Figure 物件的 vbar() 方法繪製 K 棒 : 

>>> fig.vbar('date', pd.Timedelta(hours=12), 'open', 'close', 
       fill_color='color', line_color='black', source=source)  
GlyphRenderer(id='p1147', ...)

呼叫 HoverTool 類別的建構式建立懸停工具物件 : 

>>> hover=HoverTool(
    tooltips=[
        ("日期", "@date{%F}"),
        ("開盤", "@open"),
        ("收盤", "@close"),
        ("最高", "@high"),
        ("最低", "@low")
        ],
    formatters={'@date': 'datetime'}
    )

呼叫 Figure 物件的 add_tool() 方法將懸停工具物件加入畫布中 :

>>> fig.add_tools(hover)

這樣便可呼叫 show() 函式來顯示畫布了 :

>>> show(fig)   

Bokeh 會自動開啟瀏覽器顯示網頁來呈現畫布內容 :




完整程式碼如下 : 

# bokeh_candlestick_1.py
import pandas as pd
from bokeh.plotting import figure, show
from bokeh.models import ColumnDataSource, HoverTool

# OHLC 資料 :
data={
    'date': pd.to_datetime(['2026-05-01', '2026-05-02', '2026-05-03', '2026-05-04']),
    'open':  [100, 110, 105, 120],
    'high':  [115, 120, 110, 130],
    'low':   [95, 105, 100, 115],
    'close': [110, 105, 108, 125]
    }
df=pd.DataFrame(data)

# 定義漲跌顏色
df['color']=['#ff0000' if c >= o else '#00aa00' for o, c in zip(df.open, df.close)]
source=ColumnDataSource(df)

# 建立畫布
fig=figure(
    x_axis_type='datetime', 
    title='Bokeh K線圖', 
    width=800,
    height=400,
    #tools='pan, wheel_zoom, box_zoom, reset, save'
    )

# 繪製上下影線 (Segment 物件)
fig.segment('date', 'high', 'date', 'low', color="black", source=source)

# 繪製 K 棒 (Vbar 物件)
# width 設定為 12 小時 (以毫秒計算) 以確保條形之間有間隔
fig.vbar('date', pd.Timedelta(hours=12), 'open', 'close', 
       fill_color='color', line_color='black', source=source)

# 加入互動式懸停工具 (HoverTool)
hover=HoverTool(
    tooltips=[
        ("日期", "@date{%F}"),
        ("開盤", "@open"),
        ("收盤", "@close"),
        ("最高", "@high"),
        ("最低", "@low")
        ],
    formatters={'@date': 'datetime'}
    )
fig.add_tools(hover)

# 輸出與顯示
#output_file("candlestick.html")
show(fig)


1. 繪製從 yfinance 下載的實盤 K 線圖 : 

下面範例改從 yfinance 抓取實盤 OHLC 資料來繪製 K 線圖, 程式碼如下 :

# bokeh_candlestick_2.py
import yfinance as yf
import pandas as pd
from bokeh.plotting import figure, show
from bokeh.models import ColumnDataSource, HoverTool

# 1. 下載真實 OHLCV 資料
df=yf.download('0050.tw', start='2026-03-01', end='2026-04-30', auto_adjust=True)
# 攤平 yfinance 最新版本回傳的 MultiIndex 欄位
df.columns=df.columns.map(lambda x: x[0])
# yfinance 的日期會被設為索引, 重設為一般欄位 以利 ColumnDataSource 讀取
df=df.reset_index()  

# 2. 定義漲跌顏色 (紅漲綠跌)
df['color']=['#ff0000' if c >= o else '#00aa00' for o, c in zip(df['Open'], df['Close'])]

# 3. 建立資料來源
source=ColumnDataSource(df)

# 4. 建立畫布
fig=figure(
    x_axis_type='datetime', 
    title='0050.TW 台灣50 ETF K線圖 (2026-03 ~ 2026-04)', 
    width=800,
    height=400,
    # 加入十字游標 (crosshair) 與鎖定 X 軸縮放(xwheel_zoom) 以利看盤
    tools='xpan, xwheel_zoom, box_zoom, crosshair, reset, save'
    )
# 讓網格線淡一點,視覺更聚焦在 K 棒上
fig.grid.grid_line_alpha=0.3

# 5. 繪製上下影線
fig.segment('Date', 'High', 'Date', 'Low', color="black", source=source)

# 6. 繪製 K 棒
# Bokeh 處理時間軸時寬度用毫秒計算最為精準與穩定。12小時=12 * 60 * 60 * 1000 毫秒
width_ms=12 * 60 * 60 * 1000
fig.vbar('Date', width_ms, 'Open', 'Close', 
         fill_color='color', line_color='black', source=source)

# 7. 加入互動式懸停工具 (HoverTool)
# 加上了數值格式化 {0.00} 讓小數點對齊並順手加入成交量 (Volume)
hover=HoverTool(
    tooltips=[
        ("日期", "@Date{%F}"),
        ("開盤", "@Open{0.00}"),
        ("收盤", "@Close{0.00}"),
        ("最高", "@High{0.00}"),
        ("最低", "@Low{0.00}"),
        ("成交量", "@Volume{0,0}")
        ],
    formatters={'@Date': 'datetime'}
    )
fig.add_tools(hover)

# 8. 輸出與顯示
show(fig)

此例的關鍵之處是 yf 下載的 df 日期欄位會被設為索引, 所以必須呼叫 df.reset_index() 將日期欄位重設為一般欄位, 這樣 ColumnDataSource 物件才能讀得到, 另外, 懸停工具則添加了 yf 取得的成交量資訊, 結果如下 :




可見十字線會隨著滑鼠在畫布中移動, 當指向 K 棒時會出現懸停提示, 顯示 OHLCV 數據. 


2. 客製化的虛線十字輔助線 :   

預設的十字線是實線, 如果要改為虛線須取消 tools 參數中的 crosshair, 然後匯入 CrosshairTool, Span 這兩個類別 :

from bokeh.models import ColumnDataSource, HoverTool, CrosshairTool, Span 

其中 CrosshairTool 用來建立客製化十字輔助線, 而 Span 則用來建立具有 line_dash 屬性的 Span (跨距線) 物件, 然後把這些線覆蓋 (overlay) 到 CrosshairTool 物件上, 程式改寫如下 :

# bokeh_candlestick_3.py
import yfinance as yf
import pandas as pd
from bokeh.plotting import figure, show
from bokeh.models import ColumnDataSource, HoverTool, CrosshairTool, Span

# 1. 下載真實 OHLCV 資料
df=yf.download('0050.tw', start='2026-03-01', end='2026-04-30', auto_adjust=True)
# 攤平 yfinance 最新版本回傳的 MultiIndex 欄位
df.columns=df.columns.map(lambda x: x[0])
# yfinance 的日期會被設為索引, 重設為一般欄位 以利 ColumnDataSource 讀取
df=df.reset_index()

# 2. 定義漲跌顏色 (紅漲綠跌)
df['color']=['#ff0000' if c >= o else '#00aa00' for o, c in zip(df['Open'], df['Close'])]

# 3. 建立資料來源
source=ColumnDataSource(df)

# 4. 建立畫布
fig=figure(
    x_axis_type='datetime', 
    title='0050.TW 台灣50 ETF K線圖 (2026-03 ~ 2026-04)', 
    width=800,
    height=400,
    # 鎖定 X 軸縮放(xwheel_zoom) 以利看盤 (取消 crosshair)
    tools='xpan, xwheel_zoom, box_zoom, , reset, save'
    )
# 讓網格線淡一點,視覺更聚焦在 K 棒上
fig.grid.grid_line_alpha=0.3

# 5. 繪製上下影線
fig.segment('Date', 'High', 'Date', 'Low', color="black", source=source)

# 6. 繪製 K 棒
# Bokeh 處理時間軸時寬度用毫秒計算最為精準與穩定。12小時=12 * 60 * 60 * 1000 毫秒
width_ms=12 * 60 * 60 * 1000
fig.vbar('Date', width_ms, 'Open', 'Close', 
         fill_color='color', line_color='black', source=source)

# 7. 加入互動式懸停工具 (HoverTool)
# 加上了數值格式化 {0.00} 讓小數點對齊並順手加入成交量 (Volume)
hover=HoverTool(
    tooltips=[
        ("日期", "@Date{%F}"),
        ("開盤", "@Open{0.00}"),
        ("收盤", "@Close{0.00}"),
        ("最高", "@High{0.00}"),
        ("最低", "@Low{0.00}"),
        ("成交量", "@Volume{0,0}")
        ],
    formatters={'@Date': 'datetime'}
    )
fig.add_tools(hover)

# 8. 加入虛線十字游標 (建立水平與垂直方向的 Span 物件, 設定為 dashed 虛線)
w_span=Span(dimension="width", line_dash="dashed", line_color="gray", line_alpha=0.6, line_width=1)
h_span=Span(dimension="height", line_dash="dashed", line_color="gray", line_alpha=0.6, line_width=1)
fig.add_tools(CrosshairTool(overlay=[w_span, h_span]))

# 9. 輸出與顯示
show(fig)

結果如下 :




可見十字輔助線已經變成虛線了. 


3. 消除非交日空缺 :   

與 Plotly 一樣, Bokeh 在繪製 K 線圖時會自動把 yfinance 下載的 OHLCV 資料 X 軸的日期序列補齊, 使得 K 棒在非交易日無 K 棒出現不連續的空缺. Plotly 的 Figure 物件有內建的 rangebreaks 參數可用來隱藏這些空缺, 但 Bokeh 並沒有. 

在 Bokeh 中可以用 Index Mapping 來解決此問題. 概念是不要把 X 軸當成時間, 而是把它當成 連續的整數數列 (0, 1, 2, 3...), 然後透過一個標籤轉換器騙過使用者的眼睛, 讓 X 軸的數字顯示成對應的日期. 具體而言是透過 CustomJSTickFormatter 來實現, 但如果要維持原本 Bokeh 的 X 軸標籤顯示風格, 必須修改 Javascript 程式碼, 例如 :

# bokeh_candlestick_4.py
import yfinance as yf
import pandas as pd
from bokeh.plotting import figure, show
from bokeh.models import ColumnDataSource, HoverTool, CrosshairTool, Span, CustomJSTickFormatter

# 1. 下載真實 OHLCV 資料
df=yf.download('0050.tw', start='2026-03-01', end='2026-04-30', auto_adjust=True)
df.columns=df.columns.map(lambda x: x[0])
df=df.reset_index()

# 2. 建立連續整數序列
df['seq']=range(len(df))

# 用 Python 預先將日期轉為 Bokeh 風格的 "Apr 1" 格式
date_map={}
prev_year=None
for i, d in enumerate(df['Date']):
    # 取得月份縮寫與日期 (例如 "Apr 1")
    # strftime('%b') 會輸出英文月份縮寫,d.day 會輸出不補零的日期
    label=f"{d.strftime('%b')} {d.day}"
    # 如果是圖表的第一根 K 棒,或者是跨年的第一根 K 棒,在底下加上年份
    # 使用 \n 來讓 Bokeh 產生換行效果
    if i == 0 or d.year != prev_year:
        label += f"\n{d.year}"
    date_map[str(i)]=label
    prev_year=d.year

# 3. 定義漲跌顏色
df['color']=['#ff0000' if c >= o else '#00aa00' for o, c in zip(df['Open'], df['Close'])]

# 4. 建立資料來源
source=ColumnDataSource(df)

# 5. 建立畫布
fig=figure(
    title='0050.TW 台灣50 ETF K線圖 (完美日期標籤版)', 
    width=800,
    height=400,
    tools='xpan, xwheel_zoom, box_zoom, reset, save'
    )
fig.grid.grid_line_alpha=0.3

# 6. 使用 JavaScript 取出排版好的字串
js_code="""
    var idx=Math.round(tick).toString();
    if (date_map[idx] !== undefined) {
        return date_map[idx];
    } else {
        return "";
    }
"""
fig.xaxis.formatter=CustomJSTickFormatter(code=js_code, args={'date_map': date_map})
# 把傾斜拿掉 (設為 0) 讓標籤恢復水平,這樣帶有 \n 年份的文字才會對齊
fig.xaxis.major_label_orientation=0
# 拉開標籤與 X 軸的間距 (預設通常是 5)
fig.xaxis.major_label_standoff=15

# 7. 繪製上下影線 
fig.segment('seq', 'High', 'seq', 'Low', color="black", source=source)

# 8. 繪製 K 棒
width_val=0.8
fig.vbar('seq', width_val, 'Open', 'Close', 
         fill_color='color', line_color='black', source=source)

# 9. 加入互動式懸停工具 
hover=HoverTool(
    tooltips=[
        ("日期", "@Date{%F}"),
        ("開盤", "@Open{0.00}"),
        ("收盤", "@Close{0.00}"),
        ("最高", "@High{0.00}"),
        ("最低", "@Low{0.00}"),
        ("成交量", "@Volume{0,0}")
        ],
    formatters={'@Date': 'datetime'}
    )
fig.add_tools(hover)

# 10. 加入虛線十字游標 
w_span=Span(dimension="width", line_dash="dashed", line_color="gray", line_alpha=0.6, line_width=1)
h_span=Span(dimension="height", line_dash="dashed", line_color="gray", line_alpha=0.6, line_width=1)
fig.add_tools(CrosshairTool(overlay=[w_span, h_span]))

# 11. 輸出與顯示
show(fig)

結果如下 :




不過這樣的程式碼看起來就有點複雜了. 

2025年6月17日 星期二

Python 資料視覺化學習筆記索引

最近趁學習 Streamlit 之便複習了 Matplotlib, Seaborn, 與 Plotly 這三個 Python 資料視覺化套件, 同時還學會了 Bokeh 與 Altair 這兩個新的套件, 為了後續學習研究時查詢參考方便, 我將這五套的學習筆記做成如下索引 :

Matplotlib :



Seaborn :



Plotly :



Bokeh : 



Altair :



~ 進行中 ~

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_range 或 FactorRange),用於正確計算偏移。


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 以避免圖例遮蓋了長條. 

Python 學習筆記 : 用 Bokeh 繪製互動式圖表 (二) 散布圖

用過 Bokeh 才知道它功能強大, 所以今天繼續來測試 Bokeh 的散布圖.

本系列之前文章參考 :


Bokeh v3.4 版後官方建議使用 scatter() 方法取代先前用 circle(), square() 這些獨立方法來繪製散布圖的做法, 舊的方法仍可使用, 但執行會出現即將 deprecated 的警告. 新舊介面的對照如下表 : 


Deprecated 方法 推薦寫法(使用 scatter)
fig.circle(x, y, size=...) fig.scatter(x, y, size=..., marker="circle")
fig.square(x, y, size=...) fig.scatter(x, y, size=..., marker="square")
fig.cross(x, y, size=...) fig.scatter(x, y, size=..., marker="cross")
fig.triangle(x, y, size=...) fig.scatter(x, y, size=..., marker="triangle")
fig.diamond(x, y, size=...) fig.scatter(x, y, size=..., marker="diamond")
fig.x(x, y, size=...) fig.scatter(x, y, size=..., marker="x")
fig.asterisk(x, y, size=...) fig.scatter(x, y, size=..., marker="asterisk")
fig.star(x, y, size=...) fig.scatter(x, y, size=..., marker="star")


fig.scatter() 的參數結構如下 : 

fig.scatter(x, y=None, *, marker='circle', size=4, color='blue', alpha=1.0, legend_label=None, **kwargs)

參數說明如下表 : 


scatter() 方法的參數 說明
x x 軸數值序列(list、NumPy array 或 Pandas Series)
y y 軸數值序列
size 點的大小,單位為 pixel,可為單一數值或對應每點的序列
marker 點的形狀,如 "circle"、"square"、"triangle"、"cross"、"x"、"diamond"、"asterisk"、"star" 等
color 填色,可以為色名(如 "red")、十六進位(如 "#FF0000")或 RGB 字串(如 "rgb(255,0,0)" 或"rgba(255,0,0, 0.5)" )
alpha 透明度,0(完全透明)~1(完全不透明)
line_color 邊框顏色,預設與 color 相同
line_width 邊框寬度(px)
fill_color 內部填色,預設與 color 相同
fill_alpha 內部透明度
legend_label 圖例標籤
name 圖形物件的識別名稱,可用於選取
source 指定 ColumnDataSource 物件作為資料來源


所有顏色參數的值可以是顏色名稱例如 "red", 16 進位色碼例如 "#FF00FF", 或 RGBA 色碼例如 "rgba(255, 0, 255, 0.5)". 

例如 : 


測試 1 : 預設的圓點散布圖 [看原始碼]   

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

x=[0, 1, 2, 3, 4, 5]  # x 軸資料
y=[-5, -2, 6, 12, 4, 9]  # y 軸資料
fig=figure(width=400, height=300, title='Bokeh 散布圖')  # 建立 figure 物件
fig.scatter(x, y, marker='circle')  # 繪製圓點散布圖
show(fig)  # 顯示圖表

結果如下 :




可見預設是藍色圓點, 尺寸 4px 似乎有點小, 可用 size 參數設定 (對圓點來說是指直徑). 下面範例同時測試 size 與 color 參數 : 


測試 2 : 設定 size 與 color 參數的圓點散布圖 [看原始碼]   

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

x=[0, 1, 2, 3, 4, 5]  # x 軸資料
y=[-5, -2, 6, 12, 4, 9]  # y 軸資料
fig=figure(width=400, height=300, title='Bokeh 散布圖')  # 建立 figure 物件
fig.scatter(
    x,
    y,
    marker='circle',
    size=10,  
    color='red'
    )  # 繪製圓點散布圖
show(fig)  # 顯示圖表

此例將圓點大小設為直徑 10px, 顏色設為紅色, 結果如下 :




color 參數用來設定整個標記符號的顏色, 包含邊框與填滿顏色, 若要分別設定就要使用 line_color (邊框顏色) 與 fill_color (填滿顏色), 填滿顏色還可以用 fill_alpha 設定透明度, 例如 : 


測試 3 : 設定 line_color, fill_color 與 fill_alpha 參數的圓點散布圖 [看原始碼]   

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

x=[0, 1, 2, 3, 4, 5]  # x 軸資料
y=[-5, -2, 6, 12, 4, 9]  # y 軸資料
fig=figure(width=400, height=300, title='Bokeh 散布圖')  # 建立 figure 物件
fig.scatter(
    x,
    y,
    marker='circle',
    size=20,
    line_color='black',
    fill_color='red',
    fill_alpha=0.3
    )  # 繪製圓點散布圖
show(fig)  # 顯示圖表

此例將圓點邊框設為黑色, 填滿顏色設為紅色, 填滿透明度設為 0.3, 結果如下 :




測試 4 : 設定 line_width 與 legend_label 參數的圓點散布圖 [看原始碼]   

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

x=[0, 1, 2, 3, 4, 5]  # x 軸資料
y=[-5, -2, 6, 12, 4, 9]  # y 軸資料
fig=figure(width=400, height=300, title='Bokeh 散布圖')  # 建立 figure 物件
fig.scatter(
    x,
    y,
    marker='circle',
    size=20,
    line_width=3,
    fill_color='#ff0000',
    legend_label='測試資料'
    )  # 繪製圓點散布圖
show(fig)  # 顯示圖表

此例設定框邊線寬為 4px, 也設定了圖例文字, 結果如下 : 




由於圖例位置預設是右上方 ("top_right"), 遮住了最後一個資料點, 這可以利用 fig.legend[0].location 屬性來更改為左上或右下來避免遮住問題, 例如 : 


測試 5 : 用 fig.legend[0].location 設定圖例位置 [看原始碼]  

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

x=[0, 1, 2, 3, 4, 5]  # x 軸資料
y=[-5, -2, 6, 12, 4, 9]  # y 軸資料
fig=figure(width=400, height=300, title='Bokeh 散布圖')  # 建立 figure 物件
fig.scatter(
    x,
    y,
    marker='circle',
    size=20,
    line_width=3,
    fill_color='#ff0000',
    legend_label='測試資料'
    )  # 繪製圓點散布圖
fig.legend[0].location='top_left'  # 設定圖例位置為左上
show(fig)  # 顯示圖表

此例將圖例位置改為左上, 注意, 圖例位置必須在呼叫繪圖方法 scatter() 之後才能設定, 因為這時 Lengend 物件才會存在, 結果如下 :




除了圓點外也可以使用其他標記符號例如方點 : 


測試 6 : 用 marker='square' 繪製方點散布圖 [看原始碼]  

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

x=[0, 1, 2, 3, 4, 5]  # x 軸資料
y=[-5, -2, 6, 12, 4, 9]  # y 軸資料
fig=figure(width=400, height=300, title='Bokeh 散布圖')  # 建立 figure 物件
fig.scatter(
    x,
    y,
    marker='square',
    size=10,
    line_color='black',
    fill_color='rgba(0, 255, 255, 0.5)',
    legend_label='測試資料'
    )  # 繪製圓點散布圖
fig.legend[0].location='top_left'  # 設定圖例位置為左上
show(fig)  # 顯示圖表
 
結果如下 :