プロジェクト

全般

プロフィール

Python Bokeh

はじめに

Webブラウザ上にインタラクティブな操作が可能なグラフを表示するPythonのライブラリです。グラフの拡大・縮小・スクロール・初期表示にリセット、ホバー表示、データ系列毎のオン・オフ、などのといった操作が可能です。
また、ボタン、スライダー、選択リスト、チェックボックス、表などのウィジェット(ユーザー操作部品)を用意しており、ダッシュボードのような機能も実現できるライブラリとなっています。

Webブラウザ上ではインタラクティブなグラフ表示操作を実現するJavaScriptのBokehJSライブラリを使用し、PythonコードからBokehJSライブラリを制御するHTML/JavaScriptコードを生成します。

Bokehサーバーを併用することで、ブラウザ上の表示操作をBokehサーバー側のPythonコードで処理する構成も取れます。この構成ではJavaScriptコードを記述することなくPythonコードでアプリケーションを完結させることができます。

比較的大規模データを軽快に扱うことができます。2026年前半時点の一般的なWindows ノートPC/MacBook Airで10万点程度のデータを折線プロットさせた場合でも拡大縮小移動などの操作が軽快に行えています。

公式サイト

公式サイト: https://bokeh.org

ライセンス

BSD license

インストール

pip

pipの場合、次でインストールします。

pip install bokeh

Bokehの動作概要

Bokehライブラリを利用したグラフ表示には、次の2つの実行形態があります。

  1. Pythonプログラムを実行しHTMLファイルを生成しWebブラウザに表示
  2. BokehサーバーをPythonスクリプトを指定して実行、WebブラウザからBokehサーバーに接続し表示

HTMLファイル生成

Pythonプログラムを実行し、その中でBokehライブラリを利用してグラフをプロットすると、HTMLファイルを生成し、Webブラウザ上でそのHTMLファイルを開くと、インタラクティブ操作が可能なグラフが表示されます。Windows、Mac、LinuxデスクトップでPythonプログラムを実行すると、デフォルトのWebブラウザでHTMLファイルを開きグラフ表示するところまで自動で動きます。

このHTMLファイルのみを、PythonプログラムやPython実行環境のないPCに持っていってもインタラクティブなグラフ表示操作が可能です。

block P["Python code"] space H["HTML file"] space W["Web browser"] P--"create"-->H H--"show"-->W

グラフ表示に加えて、ボタン、スライダー、チェックボックスその他多数のユーザー操作可能な部品(Widgets)が用意されています。HTMLファイルで完結させるにはユーザー操作に対応する処理(イベントハンドラ)をJavaScriptで記述する必要があり、Pythonコード内に文字列でJavaScriptのイベントハンドラー処理を記述します。

Webサーバーとして実行

BokehサーバーをPythonプログラムを読み込み実行し、Webブラウザから接続し、Bokehサーバーとインタラクションして動作する実行形態です。この方式で実行する場合、WidgetのイベントハンドラをPythonコードで記述することができます。例えば、ユーザー操作の結果データを新たに読み直して表示を更新するといった用途で活用できます。

block P["Python Script"] ba1<["load"]>(right) B["Bokeh Server"] ba2<["HTTP"]>(x) W["Web browser"]

最初の一歩

sin関数の折れ線グラフ

sin関数を折れ線グラフにプロットします。

import numpy as np
from bokeh.plotting import figure, show

x = np.linspace(-np.pi, np.pi, 1_000_000)
y = np.sin(x)

p = figure(title="sin(x)")
p.line(x, y)
show(p)

ここでは、NumPyライブラリの ndarray 配列を2つ用いて、X軸の値とY軸の値をそれぞれの配列に格納しています。
Bokehライブラリの figure()関数を呼びFigureオブジェクトを生成します。
生成したFigureオブジェクトのline()メソッドを呼んでX軸の値、Y軸の値を渡します。
実行すると、htmlファイルを生成し、Webブラウザでそのファイルを開きます。
プロットするポイント数を数万〜数十万個としても、そこそこの速さで操作できました。

  • 生成されるHTMLファイルは、デフォルトではカレントディレクトリに実行したPythonスクリプトファイル名と同じで
    拡張子が.htmlとなります。このファイル名は、output_file関数で指定したファイル名に変更可能です。
プロットに渡すデータ型

Bokehライブラリでは、プロットに渡すデータの型として次の4つが利用可能です。

  1. Pythonのリスト型
  2. NumPy の ndarray型
  3. Bokeh の ColumnDataSource
  4. Pandas の DataFrame

1.と2.を渡した場合は、Bokehライブラリ内部で 3.のColumnDataSourceを生成します。

プロットデータの形式

上述のサンプルではプロットするデータを格納するのに簡便のためnumpyを使用しました。
Bokehライブラリでは、Python標準のリスト、NumPyデータ、BokehライブラリのColumnDataSource、PandasライブラリのDataFrameなどをプロットデータとして渡すことができます。
リストやNumPyの配列を渡すと内部でColumnDataSourceが作られます。

オフライン環境での実行

生成されたhtmlファイルには、bokehのJavaScriptライブラリファイルをインターネットから取得する記述が含まれます。

<script type="text/javascript" src="https://cdn.bokeh.org/bokeh/release/bokeh-3.8.2.min.js"></script>

インターネット接続ができない環境で、このhtmlファイルを開いてグラフ表示をする場合、JavaScriptライブラリをhtmlファイルに埋め込み生成するようにします。output_file関数でファイル生成するときのmodeを"inline"と指定すると、JavaScriptライブラリファイルをhtmlに埋め込みます。

from bokeh.plotting import figure, output_file, show
output_file('trigonometric_inline.html', mode='inline')

一般的な設定

グラフのタイトル、各軸の名称

figure関数のキーワード引数に、グラフのタイトル(title)、X軸の名称(x_axis_label)、Y軸の名称(y_axis_label)を指定します。

p = figure(title="sin(x)", x_axis_label='angle', y_axis_label='sin(x)')
グラフのタイトルの更新

p.title.text = "sin(x+t)"

と、titleではなく、title.textに更新したい文字列を設定します。

グラフの領域の大きさ

figure関数のキーワード引数に、横幅(width)、高さ(height)を指定します。

p = figure(title="sin(x)", x_axis_label='angle', y_axis_label='sin(x)', width=600, height=400)
  • デフォルト値はwidth、heightとも600です

ツールバーの設定

拡大縮小、移動などのツールバーの設定を行います。
使用するツールをfigureのキーワード引数に列挙します。

p = figure(tools="pan,wheel_zoom,box_zoom,crosshair,box_select,lasso_select,reset,save,help")

ツールの抜粋
pan:移動
wheel_zoom: マウスホイールで拡大縮小
box_zoom: 選択範囲にズーム
crosshair: マウスカーソルの位置を交点とする縦横直線表示
box_select: 範囲選択
lasso_select: マウスカーソルの移動軌跡で範囲選択
reset: 表示を初期状態に
save: グラフをPNG画像ファイルに保存
help: Bokehドキュメントサイトのリンク
zoom_in: 拡大
zoom_out: 縮小
hover: ホバーツール表示の有効・無効

ツールバーの表示位置の設定は、toolbar_locationキーワード引数に指定します。

p = figure(toolbar_location="above")

above: グラフの上
below: グラフの下
left: グラフの左
right: グラフの右

グラフの軸の種類の設定

  • X軸、Y軸の種類(x_axis_type, y_axis_type)
    p = figure(x_axis_type="datetime", y_axis_type="log")
    

    数値の他、対数スケール(log)、日時(datetime)、地図座標(mercator)
動的な変更

figureに、軸の種類を設定し作成後、別な軸の種類に変更することはできません。
擬似的に実現するなら、あらかじめ軸の設定をした2つのfigureを用意し、visible属性を変えることでfigureを差し替える方法があります。

グラフの軸の最小最大

デフォルトでは、グラフの各軸の値の最小値、最大値はプロットするデータの最小値、最大値に基づいて自動で決まります。
明示的に指定するには、figure関数のx_range、y_rangeキーワード引数にタプルで(最小値、最大値)を指定します。

figure生成時に指定
p = figure(x_range=(-2 * np.pi, 2 * np.pi), y_range(-1.2, 1.2))
figure生成後
from bokeh.models import Range1d

p = figure(...)
p.x_range = Range1d(start=0, end=4*np.pi)
p.y_range = Range1d(start=-5, end=5)
データの範囲ぴったりにグラフの軸の値の最小・最大を表示

p.x_range.range_padding = 0

デフォルト(左右にデータのない空白あり) range_paddingを0に設定

グラフの軸のグリッド線の設定

グリッド線の数は、デフォルトではデータのキリのいい数値で引かれます。
これを調整したいとき

  • BasicTicker の desired_num_ticks 引数でグリッド線の希望本数を指定。ただしヒントなので厳密にこの本数とはならず、指定した本数前後でデータのキリのいい数値になるようグリッド線が引かれます。
    p.xaxis.ticker = BasicTicker(desired_num_ticks=11)
  • FixedTicker でグリッド線を引く数値をリスト指定。ただし指定した数値以外ではグリッド線が引かれないので、グラフを縮小・パニングしたときは軸メモリとグリッド線が表示されません。
    p.xaxis.ticker = FixedTicker(ticker=[3.4e9, 3.48e9, 3.56e9, 3.64e9, 3.72e9, 3.8e9, 3.88e9, 3.96e9, 4.04e9, 4.12e9, 4.2e9])

グリッド線を固定で引き、拡大・縮小・パニングにも追従させるには、グラフ表示の範囲が変わるイベントを拾って都度FixedTickerの値を変更するコードを記述する必要があります。JavaScriptで記述可能です。

グラフの凡例名

一つのプロット領域に複数のデータ列を描画するときに、凡例を記載します。プロット関数(line等)のキーワード引数に凡例名(legend_label)を指定します。

p.line(x, y, legend_label='sin(x)')

線種の設定

line()メソッドで線種を設定するときは、キーワード引数に 破線(line_dash)、色(line_color)、透明度(line_alpha)、線幅(line_width)、を指定します。

  • line_dash
    line_dash=[4, 4] 実線の長さと線のない部分の長さを指定、[4, 1, 1, 1]で一点破線のような形状も指定可能
    line_dash='dashed' 破線の指定、他には 'solid'(実線)、'dotted'(点線)、'dotdash'(一点破線)、'dashdot'
  • line_color
    line_color='pink' 名前付きCSSカラーで指定
    line_color='#70C040' RGB値で指定

マーカーと線の表示

Matplotlibの 'o-' のように線とマーカーを表示したい場合、line()メソッドだけでは表現できないので、line()メソッドとscatter()メソッドの2つを使って重ね合わせて表現します。

p.scatter(x, sin_y, legend_label='sin(x)')
p.line(x, sin_y, legend_label='sin(x)')

legend_labelに同じ名前を指定することで、凡例のグラフ線種も線とマーカーが重ね合わせて表示されます。

マウスホバーでデータ値表示

グラフ上にマウスカーソルを持っていくとそのグラフの値をポップアップ表示するホバー機能を追加します。

簡単な例
p.line(x, y)
p.add_tools(HoverTool(tooltips=[("x", "$x"), ("y", "$y")], mode="vline"))
show(p)
  • TODO
    • figure関数のキーワード引数tooltipsで指定する例、この時mode指定は可能か?

表示例

figureオブジェクトに add_toolsメソッドでHoverToolオブジェクトを指定します。
HoverToolにはtooltips引数にホバー表示するコンテンツの定義を渡します。見出し文字列と$xか$yでX座標の値、Y座標の値を指定、複数項目のホバー表示はカンマで区切って見出しと$座標名の指定を列挙します。$座標名の部分はホバー表示の際はデータの値となります。

pandasのDataFrameを指定してプロットした場合や、ColumnDataSourceで列名とデータ列を束ねて指定しプロットした場合は、$座標 ではなく、@列名 でデータ値を指定することができます。なお、lineメソッド等にndarrayを指定した場合、列名は含まれていないのですがメソッド内部でColumnDataSourceが作られるので、そのデフォルトの列名x, yを@x, @yのように指定することもできます。

p.add_tools(HoverTool(tooltips=[("X", "@x"), ("Y", "@y")], mode="vline"))

mode="vline"を指定すると、例えばライングラフの時にライン上にマウスを寄せなくてもグラフ上にマウスカーソルを置くとそのカーソル位置がら垂直方向の点の値を表示します。他には "hline"、"mouse"が指定可能で、hlineはカーソル位置から水平方向の最寄りの点の値を表示、mouseはマウスカーソルに最寄りの点を表示し、modeのデフォルトはmouseです。

列名指定において、計算式の記述はできないので表示の数値をあらかじめ計算してDataFrameにホバー表示用の列として渡すなどの代替案があります。

表示桁数などの指定
  • $x{0.00} 小数点以下2桁
  • @$x{(0.0 a)} 数値の大きさに応じて、k, m, g のようにキロ、メガ、ギガの値で表示
    ただし、特定の桁に固定することができないので、999,900 は 999.9 kと表示、1,000,000は 1.0 m と表示されます。
複数系列をプロットする際に、特定の系列のみホバー表示
p = figure()

current = p.line(x, y)
past = p.line(px, py)

p.add_tools(HoverTool(tooltips=[("X", "@x"), ("Y", "@y")], mode="vline", renderers=[current]))
フォーマッターの指定

デフォルトのフォーマッターは numeral です。その他、datetime、printf フォーマッターが選択可能です。
また、custom フォーマッターを指定して、CustomJSHoverのcodeにJavaScriptで計算式の記述をすることができます。

to_m_fmt = CustomJSHover(code=" 
    return (value / 1000000).toFixed(2);
")
:
HoverTool(tooltips=[("x", "@x(custom)")], formatters={"@x": to_m_fmt})

複数のデータ列を1つのグラフに表示

色を個々に指定

p.line(x, sin_y, legend_label='sin(x)') # デフォルトは'blue'
p.line(x, cos_y, legend_label='cos(x)', color='red')

データ列の表示有無操作

ショック! Bokehの凡例(Legend)は標準で系列のオン・オフを切り替える機能を持っていた!

Bokehが標準で持っている機能を知らずに実装してしまったものですが、ウィジェットの使用例として残しておきます。

2つのデータ列に対して、チェックボックスでプロットの表示有無の制御をします。
ユーザー操作を受け付ける部品をBokehではWidgetと呼んでいます。今回は、CheckBoxGroupを使ってデータ列の表示有無を受け付け、JavaScriptで表示有無の設定を切り替えます。

ソースコード全体を表示

  • lineメソッドの戻り値を変数に置いて、後ほどチェックボックスのコールバックで表示有無の際に参照
  • CheckboxGroup の生成で、labels引数にチェックボックスの表示文字列を、active引数にデフォルトでチェックを付けておくチェックボックスのインデックスを指定
  • ブラウザ上で完結する処理は、CustomJSでJavaScriptコードで記述
2つの系列を表示 1つの系列を表示
凡例で系列のオンオフ

グラフ領域(figure)を作成し、系列をプロットした後、次の設定で凡例表示の項目をクリックするとグラフの系列がオンオフされます。
プロットの際はlegend_label="usage" と凡例表示を指定します。

p.legend.click_policy = "hide"

  • プロットの前に指定するとオンオフできません

Y軸の値域が異なる2つのグラフを表示

グラフ領域に2つのプロットを表示する際に、Y軸の値の範囲が異なる場合は、extra_y_ranges を設定します。

p = figure()
p.extra_y_ranges['Signal'] = Range1d(0, 1)  # 追加するY軸の値範囲を設定
extra_y_axis = LinearAxis(axis_label="normalized level", y_range_name='Signal')  # 追加するY軸の設定
p.add_layout(extra_y_axis, 'right')  # 追加するY軸をプロット領域の右側に追加

p.line(freqs, x)  # デフォルトのY軸でプロット
p.line(freqs, shape, y_range_name='Signal', color='red')  # 追加したY軸の設定でプロット
show(p)
  • Figureオブジェクトのextra_y_ranges属性にキー名を指定して追加するY軸の値域を設定
  • 追加するY軸のLinearAxisオブジェクトを生成、axis_labelキーワード引数にラベル名、y_range_nameキーワード引数にY軸の値域のキー名を指定
  • Figureオブジェクトのadd_layoutメソッドで追加するY軸のAxisオブジェクトと追加する位置を指定
  • 追加したY軸の値域を使用するプロットの y_range_nameキーワード引数にY軸の値域のキー名を指定

複数のグラフを並べて表示

row

横にグラフを並べる場合、row関数でfigureを指定します。

from bokeh.layout import row

a_fig = figure(...)
b_fig = figure(...)

show(row(a_fig, b_fig))

column

縦にグラフを並べる場合、column関数でfigureを指定します。

from bokeh.layout import column

a_fig = figure(...)
b_fig = figure(...)

show(column(a_fig, b_fig))

gridplot

複数のグラフを行列に並べて表示します。
個々のグラフを figure で作成し、glafplot に、2次元リストでfigureを指定します。

A B
C D

のように並べる場合、

a_fig = figure(...)
b_fig = figure(...)
c_fig = figure(...)
d_fig = figure(...)

p = gridplot([[a_fig, b_fig], [c_fig, d_fig]])

上下に2つ並べる場合は、gridplot([[a_fig], [c_fig]]) のように記述します。

複数グラフのX軸表示を連動

あるグラフで拡大、縮小、パニング操作をした場合に並べて表示している他のグラフも同じ操作を適用することができます。

X軸の表示を連動する場合、連動するグラフのfigureオブジェクトのx_rangeプロパティを共有します。

a_fig = figure(...)
b_fig = figure(...)
b_fig.x_range = a_fig.x_range
show(column(a_fig, b_fig))

Widgets

グラフ以外にも様々なユーザー表示操作部品が利用できます。ボタン、スライダー、チェックボックス、テキスト入力、ドロップダウンリスト、表、カラーピッカー、日付ピッカーなどなど。

表示操作部品では、操作結果の値を他の表示部品(グラフを含む)の値とリンクさせる設定や操作を受けて処理を実行するコールバック処理をコードで記述します。

実行方式でHTMLファイル生成をする場合、コールバック処理のコードもHTMLに含める必要があります。これには、HTMLで実行できるJavaScriptコードを文字列でPythonコード上に記述します。HTMLファイルにコールバック処理コードが含まれるのでHTMLファイルだけでユーザー操作が可能となります。

実行方式でBokehサーバーを実行する場合、コールバック処理はHTML上で実行するJavaScriptのほか、Bokehサーバー上で実行するPythonコードで記述することができます。

Widgets 一覧

AutocompleteInput 補完機能付きテキスト入力
Button ボタン
CheckboxButtonGroup ボタン形状のチェックボックス群
CheckboxGroup チェックボックス群
ColorPicker 色選択ダイアログから色入力
DataCube ツリー構造のテーブル
DataTable テーブル
DatePicker 日付選択ダイアログから日付入力
DateRangePicker 日付選択ダイアログから開始・終了日付入力
MultipleDatePicker 日付選択ダイアログから複数日付入力
DatetimePicker 日時選択ダイアログから日時入力
DatetimeRangePicker 日時選択ダイアログから開始・終了日時入力
MultipleDatetimePicker 日時選択ダイアログから複数日時入力
TimePicker 時刻選択ダイアログから時刻入力
DateRangeSlider 開始・終了日付スライダー
DateSlider 日付スライダー
DatetimeRangeSlider 開始・終了日時スライダー
Div HTMLの divタグ対応テキスト
Dropdown ドロップダウンリストを表示するボタン(排他選択)
FileInput ファイル選択ダイアログからファイル名(パス)入力
HelpButton ホバーまたはクリックでツールティップ表示する(?)アイコンボタン
MultiChoice 複数項目をドロップダウンリストから選択し入力
MultiSelect 縦リストを表示、複数を選択状態にできる
NumericInput 数値を入力、入力範囲を指定可能
Paragraph テキストブロックを表示、HTMLの pタグ
PasswordInput 入力した文字が伏せられるテキスト入力
PreText テキストブロックを表示、HTMLの pre タグ
Progress 進捗バー
RadioButtonGroup ラジオボタン(並んだボタンの1つだけ選択状態)
RadioGroup ラジオ(並んだ〇の一つだけ◎になる)
RangeSlider 開始・終了の値を選択するスライダー
Select ドロップダウンリストから1つだけ選択
Slider 値を入力するスライダー
Spinner 数値をスピナー(上下操作で数値が増減)入力
Switch オン・オフをスライドで表示するトグル
Tabs タブで切り替え表示するペイン
TextAreaInput 複数行のテキストを入力
TextIput 1行のテキストを入力
Toggle ボタンで押し込み状態・解放状態が切り替わる

ユーザー操作

Button

ユーザー操作をトリガーに処理を実行するときに定番のウィジェットがボタンです。

start_button = Button(label="Start")

ボタンには、役割に応じて次の種類の見栄えが用意されています。

見栄えの設定は、キーワード引数button_typeで指定します。
Button(label="Push", button_type="primary")

コールバック(Python)

Button の on_clickメソッドにコールバック関数を設定します。
ボタンが押されるとコールバック関数が呼び出されます。

button = Button(label="Start")

def start_handler():
    # ...

button.on_click(start_hander)
ボタンの非活性化の例

時間のかかる処理をボタンを押して実行する場合など、処理が完了するまで二重実行防止などのためにボタンを非活性化させたいとします。
コールバック関数の中で、次のような記述をしても表示が更新されるのはコールバック関数が終了してからとなるので非活性化されません。

button = Button(label="Start")

def start_handler():
    button.disabled = True;
    result = subprocess.run(["analyze_data"])
    button.disabled = False

button.on_click(start_handler)

そこで、次の2つの実現方法が考えられます。

  1. ボタンのコールバックでは非活性化のみ行い、時間のかかる処理を次のコールバックで行い、最初のコールバック内で次のコールバックを指定
  2. ボタンにJavaScriptコールバックでボタンを非活性化、Pythonコールバックで時間のかかる処理を実行

1. のadd_next_tick_callbackによる2つのPythonコールバックの実装イメージは次です。

button = Button(label="Start")

def start_disable_handler():
    button.disabled = True
    curdoc().add_next_tick_callback(start_process)

def start_process():
    try:
        result = subproess.run(["analyze_data"])
    finally:
        button.disabled = False

button.on_click(start_disable_handler)

2. のJavaScript、Pythonコールバック併用の実装イメージは次です。

button = Button(label="Start")
button.js_on_click(CustomJS(args=dict(btn=button), code=""" 
    btn.disabled = True;
    btn.label = "実行中...";
""")

def start_handler():
    try:
        result = subprocess.run(["analyze_data"])
        if result.returncode == 0:
            datasource.data = #...
    finally:
        button.disabled = False
        button.label = "Start" 

button.on_click(start_handler)

RangeSliderでX軸の表示範囲を変更

グラフのX軸の表示範囲を、RangeSliderを用いて範囲指定できるようにします。
コード(抜粋)を示します。

X_MIN = -10*np.pi
X_MAX = 10*np.pi
X_RANGE_DEFAULT = (-np.pi, np.pi)

fig = figure(title="sin(x)")
fig.line(x, y)

x_slider = RangeSlider(
    start=X_MIN, end=X_MAX, value=X_RANGE_DEFAULT, step=0.1, title="ranges of x axis" 
)
x_slider.js_link("value", fig.x_range, "start", 0)
x_slider.js_link("value", fig.x_range, "end", 1)
  • グラフ領域を生成するfigure関数のキーワード引数x_rangeで、x_rangeで軸の表示範囲の開始・終了値をタプルで指定
  • RangeSliderウィジェットを生成、キーワード引数start/endでスライダーの下限・上限値、valueで表示範囲の開始・終了値の初期値、stepで値変更の刻み、titleでスライダーにタイトルを設定
  • RangeSliderの値を、グラフ領域のX軸の表示範囲と紐づけるため、js_linkメソッドでRangeSlinderのvalue属性(タプルで(開始値、終了値)を取る)を、グラフ領域 fig の x_range の start/end属性と紐づけています。最後の引数は attr_selectorで、スライダーのvalueがタプルの時、どの値を紐づけるかをインデックスで指定します。

ファイル選択(FileInput)

ユーザー側にあるファイルをWebブラウザのファイル選択ダイアログで指定し、ブラウザ経由でサーバーにファイル内容を送信します。

import base64
from bokeh.models import FileInput
import pandas as pd

file_input = FileInput(accept=".csv")  # ①

def upload_handler(attr, old, new):
    raw_bytes = base64.b64decode(new)  # ③
    data = io.BytesIO(raw_bytes)  # ④
    df = pd.read_csv(data, names=["fc", "bandwidth"], header=None)  # ⑤

file_input.on_change("value", upload_handler)  # ②
ウィジェットの表示(選択前) ウィジェットの表示(選択後)
  • ①ファイル選択ダイアログを、拡張子 .csv のファイルのみ選択可能で生成
  • ②ファイル選択がなされたとき、ファイル内容を取得しPythonのコールバックを呼ぶ
  • ③Webブラウザで開いたファイルはサーバーにBASE64符号化されて到達するのでBASE64符号をデコードし元のデータ(バイナリ)を取得
  • ④バイナリデータからBytesIOでファイルストリーム(バイナリモード)を取得
  • ⑤今回はCSV形式ファイルをPandasで読み込み。ファイル内容はデータのみとし見出し行(Header)がない場合の例
    • テキストファイルがUTF-8以外の場合、encodingキーワード引数で符号化種類を指定(例、encoding="cp932")

ファイル選択(サーバー側)

サーバー側でしかアクセスできないファイルを読み込ませたいときは、サーバー側でファイルリストを作成しユーザーに選択させます。
ここでは、Select ウィジェットを使ってファイル選択を実装します。

from bokeh.models import Select

file_list = ["/a/b.csv", "/a/c.csv", "/a/d.csv"]
file_select = Select(title="ファイル選択", file_list[0], options=file_list)
  :
def update_plot(attr, old, new):
    selected = file_select.value
    source.data = load_csv(selected) # 別途load_csv関数を定義
    p.title = selected

file_select.on_change("value", update_plot)

レイアウト

画面配置

画面へのグラフ(figure)、ウィジェットの配置には、次のレイアウト部品があります。

  • Row
    指定したウィジェット群を横並びに配置
  • Column
    指定したウィジェット群を縦並びに配置
  • Grid

部品の間に隙間を入れる

row(b1, b2, spacing=20)

このとき、row自身の大きさは、内容物を並べて配置する大きさとなります。

画面の大きさに部品の大きさを追従

画面の横幅に合わせて row を拡張するには、sizing_mode で横幅を広げる stretch_width を指定します。
row(b1, b2, sizing_mode="stretch_width")

ただし、b1, b2 は、左からそれぞれの大きさに応じて並ぶので右側が空となります。
例えば、b1, b2 を右寄せにしたいときは、
row(Spacer(sizing_mode="stretch_width"), b1, b2, sizing_mode="stretch_width")
と、rowのsizing_modeをstretch_widthとして画面幅に合わせて広げるとともに、左端に置いたSpacerのsizing_modeをstretch_widthとして可能な限り広げるようにします。

row の専有領域を確認する

rowが画面上でどの領域を占めているかを確認するコード例です。

r = row(btn1, btn2, text)
r.styles(
    "border": "2px solid red",
    "background-color": "lightgrey" 
}

ウィジェット

ウィジェットのサイズ

大きさの指定

ウィジェット生成時にキーワード引数 width と height でウィジェットの大きさをピクセル数で指定します。
btn = Button(label="Hello", width=100, height=50)

マージンの指定

ウィジェットの外側の余白(margin)を指定可能です。
btn = Button(label="Hello", margin=(8, 2, 8, 2))
上、右、下、左 の外側余白をタプルで指定します(順序はCSSと同様)。
margin=10 と単一の値で指定すると4方向すべてに適用します。

タイトルをwidgetの上以外に配置

title属性を持つSelectなどは、widgetの上に名称などを表示できます。しかし、位置を左右や下には変更できないので、そのときはテキストとwidgetをrowで並べます。rowで並べたとき、デフォルトでは高さが揃っていないのて、テキストのalignを"center"にします。

Bokehサーバー

起動方法

pythonファイルを指定して実行

work$ bokeh serve myplot.py --show
  :
  • --show オプションを指定するとそのマシン上でWebブラウザを起動しbokehサーバーのポートに接続するアドレスでページを表示
コマンドラインオプション

bokeh serve myplot.py --show --args /path/to/data -d

Pythonスクリプトへコマンドライン引数を渡すことができます。--argsオプションの後ろにコマンドラインオプションを記述します。

ディレクトリを指定して実行

work$ ls myapp
main.py
work$ bokeh serve myapp
  :

Webページ

ページタイトルの設定

デフォルトでは、Bokehサーバーから送られるWebページのタイトルは、「Bokeh Application」です。タイトルの設定は次の指定で可能です。
curdoc().title = "Signal Plot"

雑多(未整理)事項

グラフの操作

系列の表示制御

  • legend(凡例)をクリックすると、プロットを薄い色にする mute 操作も可能
    p.legend.click_policy = "mute"
    このとき、mute状態の表示色をあらかじめ設定する
    p.line(x, y, legend_label="sin(x)", muted_color="grey", muted_alpha=0.25)

グラフの表示

凡例の位置

凡例(legend)は、デフォルトではグラフ領域の内側、上右側に配置されます。

p = figure()
p.legend.location = "top_left" 

グラフ領域内で凡例を表示する位置は、figureオブジェクトの legend.location 属性に次の文字列で指定可能です。

"top_right"  "top_left"  "bottom_right"  "bottom_left"  "center_right"  "center_left" 
"top"  "bottom"  "center"  "left"  "right" 

グラフ領域外で凡例を表示する場合は、簡易的には次のコードで設定可能です。

p = figure()
p.line(...)
p.line(...)
legend = p.legend[0]
p.add_layout(legend, "right")

レイアウト

データソース

データソース(ColumnDataSource)を使用すると、データソースのデータを変更することでプロットに反映することができます。

データソースを使ってプロット

データソース(ColumnDataSource)を作ってsin関数をプロットします。

import numpy as np
from bokeh.models import ColumnDataSource
from bokeh.plotting import figure, show

x = np.linspace(-np.pi, np.pi, 1_000)
sin_y = np.sin(x)

source = ColumnDataSource(data=dict(x=x, y=sin_y))

p = figure(title="sin(x)", width=640, height=400)
p.line("x", "y", source=source)

show(p)

プロットするデータを、ColumnDataSourceオブジェクトのdata属性に保持します。上述サンプルコードでは、コンストラクタでキーワード引数dataにdict型で X/Yデータを渡しています。キーの名前は任意で構いません。データソースをlineメソッドに渡す場合は引数の先頭2つに、データソースのdata属性に設定したdictのキー名(x, y)をそれぞれ指定します。

データソースのデータを入れ替え

まずsin関数とcos関数の系列を作成します。最初にsin関数をプロットし、ボタンを押すとcos関数をプロットします。
ここで、ボタンを押すとデータソースのデータをsin関数からcos関数に変更します。


import numpy as np
from bokeh.layouts import row
from bokeh.models import ColumnDataSource, Button, CustomJS
from bokeh.plotting import figure, output_file, show

output_file('trigonometric_datasource_inline.html', mode='inline')

x = np.linspace(-np.pi, np.pi, 1_000)
sin_y = np.sin(x)
cos_y = np.cos(x)

source = ColumnDataSource(data=dict(x=x, y=sin_y))

p = figure(title="sin(x) then cos(x)", width=640, height=400)
p.line("x", "y", source=source)

button = Button(label="Change")
callback = CustomJS(
    args=dict(
        source=source,
        cos_y=cos_y,
    ),
    code=""" 
        source.data["y"] = cos_y
        source.change.emit()
    """ 
)
button.js_on_click(callback)
layout = row(p, button)
show(layout)

Bokehのユーザー操作部品(Widgets)の一つ Button を作成し、ボタンを押すとコールバック関数を実行、その中でデータソースをsin関数からcos関数に変更します。
Bokehが生成するHTMLの中でボタンの処理をするため、コールバック処理はJavaScriptで記述します。
コールバックを記述するCustomJSクラスのコンストラクタに、コールバック処理でアクセスするデータをdict型に詰めて渡し、コールバックJavaScriptコードを文字列で渡します。
ButtonがクリックされるとCustomJSオブジェクトを実行するようコールバックを紐づけます。

データソースのデータに追加

データソースにデータを追加するときは、ColumnDataSourceのstreamメソッドを呼び追加のデータを渡します。

デバッグ

CustomJSのJavaSctit

Webブラウザの開発者ツールでコンソールを開き、
console.log("デバッグメッセージ");
で確認します。

ロギング

bokehサーバー形態のロギング

bokehサーバーは、アプリケーションより先にloggingを使用してbasicConfigでログ設定をしているので、アプリケーション側でbasicConfigを使った設定をすると複数設定となってしまい制約が生じます。また、bokehサーバーはログレベルをWARNINGに設定しているのでアプリケーション側で設定をしないとロギングが使い物になりません。

ログレベルを変更する方法としては、

  1. basicConfigでキーワード引数force=Trueを指定して設定を上書き
  2. パッケージ階層を利用してログレベル変更

1.は、Bokehライブラリ側のログレベルも変更されるのでログメッセージが増えてしまいます。
2.は、basicConfigではなく、getLoggerでパッケージ名・モジュール名の階層構造を利用して上位の階層にログレベルを設定します。ただし、Bokehのサーバー起動では、main.py が特殊な形態で実行され、それ以外のモジュールはトップレベルになってしまうため、__name__、__package__ が利用できません。アプリケーションで パッケージ名.モジュール名の文字列を作成してgetLoggerに渡す必要があります。

basicConfigで上書き
import logging

logging.basicConfig(level=logging.DEBUG, force=True)

ただしこの設定では、Bokehライブラリが設定した内容も消えてしまい、タイムスタンプが表示されないなどの弊害があるので、レベル以外もきちんと設定をした方がよいです(ちょっと面倒)。

パッケージ・モジュール名階層を利用

アプリケーションをパッケージとして構成します。例えば次のようにディレクトリを作ります。

work
  +-- myapp
        +-- main.py
        +-- data.py
        +-- logutil.py

エントリポイントのPythonファイル名は、main.py とする必要があります。
パッケージとして実行するときは、bokeh サーバーに渡すアプリケーションプログラムをファイル名ではなくディレクトリ名で指定して実行します。

work $ bokeh serve --show myapp

アプリケーション側の名前階層、ログレベル設定をするユーティリティ関数を定義します。

from pathlib import Path
import logging

APP_NAME = Path(__file__).parent.name

def get_logger(file):
    name = Path(file).stem
    return logging.getLogger(f"{APP_NAME}.{name}")

def set_app_level(level):
    logging.getLogger(APP_NAME).setLevel(level)

各モジュールでロガーを取得するには、

import logutil

logger = logutil.get_logger(__file__)

main.pyでアプリケーション全体のログレベル設定

import logging
import logutil

logutil.set_app_level(logging.DEBUG)

とします。


4日前に更新