import numpy as np
import pandas as pd
from bokeh.io import output_notebook, show
from bokeh.models import ColumnDataSource
from bokeh.plotting import figure
import os
os.environ["BOKEH_MINIFIED"] = "false"
output_notebook()
Bokeh-JavaScriptコード#
Bokehの描画とインタラクティブ機能はブラウザで実行されるJavaScriptライブラリによって実現されています。Pythonでチャートを生成する際、ユーザーが作成したJavaScriptプログラムを追加することで、チャートのインタラクティブ機能を強化できます。このセクションでは、そのようなJavaScriptプログラムの作成方法を紹介します。ブラウザでJavaScriptプログラムをデバッグしやすくするため、Bokehを読み込む前に環境変数BOKEH_MINIFIED
を"false"
に設定することをお勧めします。これにより、output_notebook()
を実行すると非圧縮版のJavaScriptライブラリが読み込まれます。JavaScriptプログラムをデバッグする際、より詳細なコード情報を確認できます。
import os
from bokeh.io import output_notebook
os.environ["BOKEH_MINIFIED"] = "false"
output_notebook()
Notebookで以下のJavaScriptプログラムを実行し、出力テキストにColumnDataSource
が含まれていれば、非圧縮版JavaScriptライブラリが正しく読み込まれています。プログラムではBokeh.Models(name)
を使用してname
という名前のモデルクラスを取得し、そのクラス名を表示しています。
%%javascript
let ColumnDataSource = Bokeh.Models["ColumnDataSource"];
element.append(ColumnDataSource.name)
JavaScriptプログラムのデバッグ#
まず、ブラウザでJavaScriptプログラムをデバッグする方法を簡単に紹介します。JavaScriptでチャートオブジェクトを簡単に取得できるようにするため、チャート作成時にid
パラメータでチャートIDを指定できます。また、描画要素作成時にはname
パラメータでその名前を指定できます。
from bokeh.models import ColumnDataSource
source = ColumnDataSource(data=dict(x=[1, 2, 3], y=[1, 3, 2]))
fig = figure(height=250)
fig._id = "my_fig"
line = fig.line("x", "y", source=source, name="my_line")
show(fig)
Notebookで上記のプログラムを実行後、ブラウザで[Ctrl+Shift+I]ショートカットを押してデバッグウィンドウを開きます。Consoleタブに切り替え、以下のJavaScriptプログラムを実行します。Consoleに折れ線のX-Y軸座標データが表示されます。プログラムで使用する変数はグローバル変数なので、Consoleでfig
、line
、source
などの変数名を使って内容を確認できます。
fig = Bokeh.index["my_fig"].model; //❶
line = fig.select_one("my_line"); //❷
source = line.data_source; //❸
console.log(source.data);
❶JavaScriptでは、各チャートはBokeh.index
内のPlotView
オブジェクトに対応し、そのmodel
プロパティはPythonのfig
に対応するオブジェクトです。❷fig.select_one()
メソッドを呼び出してname
プロパティが"my_line"
のモデルオブジェクトを取得します。これはPythonのline
オブジェクトに対応し、GlyphRenderer
タイプです。❸そのdata_source
プロパティを通じて、Pythonのsource
に対応するColumnDataSource
オブジェクトを取得します。この例からわかるように、BokehはJavaScript内でPythonと同じオブジェクト構造を作成します。デバッグウィンドウのスクリーンショットは次のグラフの通りです。
js_on_event()コールバックコード#
BokehのモデルクラスModel
はjs_on_change()
とjs_on_event()
メソッドを定義しています。js_on_change()
メソッドを使用すると、JavaScriptでモデルオブジェクトに属性値変更時のコールバックコードを追加できます。一方、js_on_event()
はモデルオブジェクトにインタラクションイベントのコールバックコードを追加できます。コールバックコードではcb_obj
を通じてイベント関連情報を取得できます。すべてのイベントクラスはbokeh.events
モジュールで定義されており、cb_obj
の属性名はこれらのイベントオブジェクトの属性名と同じです。例えば、以下にマウスダブルクリックイベントDoubleTap
のドキュメントを表示します。ドキュメントから、cb_obj
にはx, y, sx, sy
の4つの属性があり、x, y
はデータ座標系での座標、sx, sy
はスクリーン座標系でのピクセル座標であることがわかります。
from bokeh import events
print(events.DoubleTap.__doc__)
Announce a double-tap or double-click event on a Bokeh plot.
Attributes:
sx (float) : x-coordinate of the event in *screen* space
sy (float) : y-coordinate of the event in *screen* space
x (float) : x-coordinate of the event in *data* space
y (float) : y-coordinate of the event in *data* space
以下の例では、ユーザーがマウスで曲線を描画できます。チャート上でマウスをダブルクリックすると曲線の描画が開始され、再度ダブルクリックするまでマウスの移動軌跡が記録されます。プログラムでは、ダブルクリックイベントDoubleTap
とマウス移動イベントMouseMove
のコールバックコードを使用し、データソースに座標点を追加することでマウス描画を実現しています。❶source
はマウス軌跡を保存するデータソースで、そのtags
属性には軌跡の色リストと現在の描画状態が保存されています。複数の図形要素がこのデータソースに接続されています。❷point_source
データソースはマウスの現在位置を保存するために使用され、散布図要素がこのデータソースに接続されています。ダブルクリックイベントとマウス移動イベントのコールバックコードはどちらもcallback
です。
イベントの関連情報はcb_obj
に保存されています。例えば、event_name
属性にはイベント名が、x
とy
属性にはデータ座標系でのマウス座標が保存されています。❹マウスダブルクリック時、event_name
属性が’doubletap’の場合、現在の描画状態が’normal’であれば曲線描画を開始します。データソースのxs
とys
列に新しいリストを追加し、colors
列に新しい色値を追加します。また、カーソルを表すpointer
描画オブジェクトを表示し、現在の描画状態を’draw’に設定します。マウスダブルクリック時、現在の描画状態が’draw’であれば曲線描画を終了します。この時、カーソルを非表示にし、描画状態を’normal’に設定します。❺event_name
属性が’mousemove’の場合、カーソルデータソースの座標を更新します。現在の描画状態が’draw’であれば、マウスの現在座標をデータソースpoint_source
のxs
とys
列の最後のリストに追加します。最後に、両データソースのchange.emit()
メソッドを呼び出してチャートを再描画します。
from bokeh.models import CustomJS
from bokeh.events import MouseMove, DoubleTap
from bokeh.palettes import Category20_20
fig = figure(
active_drag=None,
x_range=(-10, 10),
y_range=(-10, 10),
frame_width=500,
frame_height=300,
)
source = ColumnDataSource(
data=dict(xs=[], ys=[], colors=[]), tags=[dict(colors=Category20_20, mode="normal")]
) # ❶
pointer_source = ColumnDataSource(data=dict(x=[0], y=[0])) # ❷
lines = fig.multi_line(
"xs", "ys", line_color="colors", source=source, line_width=2
) # ❶
pointer = fig.scatter(
"x", "y", source=pointer_source, visible=False, marker="cross", size=10, color="red"
) # ❷
callback = CustomJS( # ❸
args=dict(source=source, pointer=pointer),
code="""
let data = source.data;
let info = source.tags[0];
if(cb_obj.event_name == 'doubletap'){
if(info.mode == 'normal'){ //❹
let colors = info.colors;
data.xs.push([]);
data.ys.push([]);
data.colors.push(colors[data.xs.length % colors.length]);
pointer.visible = true;
pointer.data_source.change.emit();
info.mode = 'draw';
}
else{
info.mode = 'normal';
pointer.visible = false;
}
}
else if(cb_obj.event_name == 'mousemove'){ //❺
pointer.data_source.data["x"] = [cb_obj.x];
pointer.data_source.data["y"] = [cb_obj.y];
if(info.mode == 'draw'){
let xs = data.xs;
let ys = data.ys;
xs[xs.length-1].push(cb_obj.x);
ys[ys.length-1].push(cb_obj.y);
source.change.emit();
pointer.data_source.change.emit();
}
}
""",
)
fig.js_on_event(DoubleTap, callback)
fig.js_on_event(MouseMove, callback)
show(fig)
データトランスフォーマー#
データ可視化では、データが直接表示に適さない場合がよくあります。この場合、データを適切に変換する必要があります。このデータ変換はPythonで計算することも、データトランスフォーマーを使用してJavaScriptで計算することもできます。例えば、数値を色に変換したり、データにジッターを追加して描画点が重ならないようにするなどの演算はJavaScriptで実装できます。以下の例では、dodge("y", 1)
はデータソースsource
の"y"
列の値に1を加えた後、赤い曲線のY軸座標値として使用します。
from bokeh.transform import dodge
x = np.linspace(0, 2 * np.pi, 100)
y = np.sin(x)
source = ColumnDataSource(data=dict(x=x, y=y))
fig = figure(frame_width=400, frame_height=250)
fig.line("x", "y", source=source)
line = fig.line("x", dodge("y", 1), color="red", source=source)
show(fig)
トランスフォーマーを使用する場合、図形オブジェクトの属性y
はデータソースの列名を表す文字列ではなく、データ変換を記述する辞書になります。これはデータソースのy
列に対してDodge
変換を行うことを示します。Dodge
はデータトランスフォーマーのモデルクラスで、dodge()
はトランスフォーマーを作成しやすくするヘルパー関数です。
line.glyph.y
Field(field='y', transform=Dodge(id='p1215', ...), units=Unspecified)
複数の点の座標が完全に同じ場合、散布図では点の分布状態を正しく表示できません。この場合、jitter()
トランスフォーマーを使用して点の座標にジッターを追加できます。例えば、以下のプログラムではnp.round()
を使用して乱数を四捨五入しているため、これらのランダムな点は次のグラフ(左)に示すような等間隔グリッド上に分布します。jitter()
を使用して点のX軸座標とY軸座標に一定のジッターノイズを追加すると、次のグラフ(右)に示すような分布を明確に観察できます。
from bokeh.transform import jitter
from bokeh.layouts import row
x, y = np.round(np.random.normal(scale=0.2, size=(2, 500)), 1)
source = ColumnDataSource(data=dict(x=x, y=y))
fig1 = figure(frame_width=250, frame_height=250)
fig1.scatter("x", "y", source=source, alpha=0.3)
fig2 = figure(frame_width=250, frame_height=250)
fig2.scatter(
jitter("x", 0.1, distribution="normal"),
jitter("y", 0.1, distribution="normal"),
source=source,
alpha=0.3,
)
layout = row(fig1, fig2)
show(layout)
以下の例では、データソースのデータを使用して散布点の塗りつぶし色(fill_color
)を制御し、その結果は次のグラフ(左)の通りです。❶linear_cmp()
は数値を色に変換するトランスフォーマーを作成します。列名以外の3つのパラメータは、色リスト、最小値、最大値です。データソースのz
列の数値をカラーマップViridis
の色に変換します。❷散布点のfill_color
属性にカラートランスフォーマーを設定します。❸カラーバーColorBar
オブジェクトを使用すると、数値と色の関係を表示できます。そのcolor_mapper
はカラーマッピングを表すColorMapper
オブジェクトです。
from bokeh.transform import linear_cmap
from bokeh.palettes import Viridis
from bokeh.models import ColorBar
x, y = np.random.normal(scale=0.2, size=(2, 500))
z = (x**2 + y**2) ** 0.5
source = ColumnDataSource(data=dict(x=x, y=y, z=z))
fig_cmap1 = figure(frame_width=400, frame_height=300)
cmap = linear_cmap("z", Viridis[256], z.min(), z.max()) # ❶
c = fig_cmap1.scatter(
"x", "y", fill_color=cmap, line_color=None, source=source, alpha=1, size=6
) # ❷
colorbar = ColorBar(
color_mapper=cmap["transform"], # ❸
label_standoff=12,
border_line_color=None,
location=(0, 0),
)
fig_cmap1.add_layout(colorbar, "right")
上記のlinear_cmap()
は連続的な数値を連続的に変化する色に変換します。一方、factor_cmap()
はカテゴリデータを色に変換でき、その結果は次のグラフ(右)の通りです。列名以外の2つのパラメータは、カラーマップとカテゴリ候補値リストです。カテゴリ値をカラーマップ内の対応するインデックスの色に変換します。❶iris.csv
データファイルを読み込む際、species
列のデータタイプをカテゴリタイプに設定します。❷カテゴリタイプの列はcat.categories
ですべてのカテゴリ値を取得できます。この方法を使用すると、1つの散布図オブジェクトで複数の異なるカテゴリの分布状況を表示できます。
from bokeh.transform import factor_cmap
from bokeh.palettes import Category10
df = pd.read_csv("data/iris.csv", dtype={"species": "category"}) # ❶
source = ColumnDataSource(data=df)
fig_cmap2 = figure(frame_width=400, frame_height=300)
cmap = factor_cmap("species", Category10[10], df.species.cat.categories) # ❷
c = fig_cmap2.scatter(
"sepal_length",
"sepal_width",
fill_color=cmap,
legend_field="species",
line_color=None,
source=source,
alpha=1,
size=6,
)
前述の各種変換器のJavaScriptプログラムは既にBokehに含まれており、手動でJavaScriptプログラムを記述する必要はありません。CustomJSTransform
クラスを使用すると、カスタムコードの変換器を作成できます。CustomJSTransform
オブジェクトでは、func
とv_func
属性が計算用のJavaScriptコードを保持します。func
のコードでは、変数x
に現在変換が必要な単一の値が保持され、コードは変換後の値を返します。v_func
のコードでは、xs
に変換が必要な配列全体が保持され、コードは変換後の配列を返します。以下の例では、❶CustomJSTransform
オブジェクトを作成し、関数\(f(x) = \sin(x) \cdot \sin(10 x)\)の値を計算します。この変換器はデータソースの列データを処理するために使用されるため、func
属性のコード定義を省略できます。❷transform()
を使用してCustomJSTransform
オブジェクトをx
という名前の列に適用します。結果は次のグラフ(左)のようになります。
layout_cmap = row(fig_cmap1, fig_cmap2)
show(layout_cmap)
from bokeh.transform import transform
from bokeh.models import CustomJSTransform
x = np.linspace(0, 2 * np.pi, 1000)
source = ColumnDataSource(data=dict(x=x))
fig_cjs1 = figure(frame_width=400, frame_height=250)
transform_sin = CustomJSTransform( # ❶
func="return Math.sin(x) * Math.sin(10*x);",
v_func="return xs.map(x=>Math.sin(x) * Math.sin(10*x));",
)
fig_cjs1.line("x", transform("x", transform_sin), source=source); # ❷
上記の例では、v_func
属性のコードの入力xs
はデータソースの特定の列データのみです。CustomJSTransform
オブジェクトのargs
属性を設定することで、データソースのすべての列データに直接アクセスできます。args
属性の使用方法はCustomJS
と同じです。以下の例では、source
モデルオブジェクトをargs
属性に渡し、v_func
コード内でsource
変数を通じてデータにアクセスします。プログラムはx
列とy
列の平方和の平方根を返し、この戻り値が各散布点のサイズを決定します。結果は次のグラフ(右)のようになります。
x, y = np.random.normal(scale=0.2, size=(2, 500))
source = ColumnDataSource(data=dict(x=x, y=y))
fig_cjs2 = figure(frame_width=400, frame_height=300)
transform_dist = CustomJSTransform(
args=dict(source=source),
v_func="""
let data = source.data;
let ys = data.y;
return xs.map((x, i) => {
let y = ys[i];
return 10 * Math.sqrt(x*x + y*y);
})
""",
)
c = fig_cjs2.scatter(
"x",
"y",
size=transform("x", transform_dist),
line_color=None,
source=source,
alpha=1,
)
layout_cjs = row(fig_cjs1, fig_cjs2)
show(layout_cjs)
データソースフィルター#
JavaScriptではデータソースのデータをフィルタリングすることもできます。以下の例では、ドロップダウン選択ボックスを使用して散布点の分類を選択できます。❶ドロップダウン選択ボックスを作成し、その候補値リスト属性options
はデータテーブルのspecies
列の選択可能な値であり、現在の値を候補リストの最初の要素に設定します。❷GroupFilter
オブジェクトは、データソース内の特定の分類のデータをフィルタリングするために使用されます。属性column_name
はデータソースの列名であり、属性group
はその列のフィルタリング値です。column_name
列の値がgroup
属性と等しいすべてのデータをフィルタリングします。フィルターはビューオブジェクトCDSView
のfilters
属性に配置され、このビューのsource
属性はフィルタリングされるデータソースです。❹フィルターが正しく機能するためには、図形を作成する際にview
パラメータでビューオブジェクトを指定する必要があります。❺ドロップダウン選択ボックスの値が変更されると、callback
内のコードが実行されます。❻このコードでは、cb_obj
はコールバックコードをトリガーするオブジェクト、つまりドロップダウン選択ボックスを表すオブジェクトです。cb_obj.value
はドロップダウン選択ボックスの現在の値であり、この値を使用してフィルターのgroup
属性を設定します。❼最後に、データソースのchange.emit()
を呼び出してチャートの表示を更新する必要があります。結果は次のグラフ(左)のようになります。
from bokeh.layouts import column
from bokeh.models import CDSView, GroupFilter, Select
df = pd.read_csv("data/iris.csv", dtype={"species": "category"})
categories = df.species.cat.categories.tolist()
select = Select(options=categories, value=categories[0]) # ❶
source = ColumnDataSource(data=df)
group_filter = GroupFilter(column_name="species", group=select.value) # ❷
view = CDSView(filter=group_filter) # ❸
fig_filter1 = figure(frame_width=400, frame_height=300)
c = fig_filter1.scatter(
"sepal_length",
"sepal_width",
line_color=None,
source=source,
view=view,
alpha=1,
size=6,
) # ❹
callback = CustomJS(
args=dict(filter=group_filter, source=source),
code="""
filter.group = cb_obj.value; //❻
source.change.emit(); //❼
""",
)
select.js_on_change("value", callback) # ❺
layout_filter1 = column(select, fig_filter1)
CustomJSFilter
オブジェクトを使用すると、カスタムコードでデータソースのデータをフィルタリングできます。そのargs
とcode
パラメータはCustomJS
と同じです。その戻り値はデータソースのデータをフィルタリングするために使用され、戻り値のタイプは整数配列またはブール配列であり、その使用方法はNumPy配列のインデックス配列と同じです。以下の例では、❶範囲スライダーコントロールRangeSlider
を使用してpetal_length
列の範囲を選択します。❷CustomJSFilter
オブジェクトが範囲スライダーの値を取得できるように、スライダーオブジェクトをargs
属性に渡します。❸カスタムコード内では、source
変数を使用してフィルタリングされるデータソースを取得できます。❹petal_length
列の各データに対して無名関数を呼び出し、スライダーで指定された範囲内にあるかどうかを判断します。ここでは、Array.from(data)
を使用して最初にdata
をJavaScriptの配列オブジェクトに変換し、その後map()
メソッドを使用してブール配列を取得する必要があることに注意してください。結果は次のグラフ(右)のようになります。
from bokeh.models import CustomJSFilter, RangeSlider
source = ColumnDataSource(data=df)
fig_filter2 = figure(frame_width=400, frame_height=300)
slider = RangeSlider( # ❶
start=df.petal_length.min(),
end=df.petal_length.max(),
value=(df.petal_length.min(), df.petal_length.max()),
step=0.1,
title="petal length",
)
func_filter = CustomJSFilter( # ❷
args=dict(slider=slider),
code="""
let data = source.data['petal_length']; //❸
let [start, end] = slider.value;
let res = Array.from(data).map(v => start <= v && v <= end); //❹
return res;
""",
)
view = CDSView(filter=func_filter)
c = fig_filter2.scatter(
"sepal_length",
"sepal_width",
fill_color=linear_cmap(
"petal_length", Viridis[256], df.petal_length.min(), df.petal_length.max()
),
line_color=None,
source=source,
view=view,
alpha=1,
size=6,
)
callback = CustomJS(args=dict(source=source), code="source.change.emit();")
slider.js_on_change("value", callback)
layout_filter2 = column(slider, fig_filter2)
layout_filter = row(layout_filter1, layout_filter2)
show(layout_filter)
カスタム軸目盛ラベル#
FuncTickFormatter
はJavaScriptコードを使用して軸の目盛ラベルを計算できます。以下の例では、さまざまな可能性のフレーズの確率分布を比較し、その効果は次のグラフのようになります。Y軸の目盛ラベルは各フレーズであり、曲線はフレーズのカーネル密度推定を示します。❶まず、Bokehのデモデータベースからprobly
を読み込みます。これはDataFrame
オブジェクトであり、各列はフレーズの確率評価です。❷gaussian_kde()
を使用して、各列データに基づいてカーネル密度推定でその確率分布を計算します。比較を容易にするために、カーネル密度推定曲線の最大値を2に設定します。
from bokeh.sampledata.perceptions import probly # ❶
from scipy.stats import gaussian_kde
import numpy as np
x = np.linspace(0, 100, 200)
df_kde = probly.apply(lambda s: gaussian_kde(s)(x)).set_index(x) # ❷
df_kde = df_kde * (2 / df_kde.max()) # ❸
❹ループ内でdf_kde
の各列データに対応する曲線を作成し、曲線のY軸座標はdodge()
を使用して異なる値にオフセットします。❺FixedTicker
を使用してY軸目盛の位置を設定します。❻FuncTickFormatter
を使用してY軸の目盛ラベルを設定します。その使用方法はCustomJS
と同じです。プログラムではlabels
変数を使用してY軸目盛位置を表すリストを受け取り、JavaScriptでそのリストを使用して目盛ラベルを取得します。目盛の位置はtick
変数で取得できます。
from bokeh.models import FixedTicker, CustomJSTickFormatter
from bokeh.palettes import Category20_20
source = ColumnDataSource(df_kde)
fig_tick = figure()
for i, col in enumerate(df_kde.columns):
fig_tick.line(
"index",
dodge(col, i),
source=source,
color=Category20_20[i],
line_width=2,
alpha=0.6,
) # ❹
fig_tick.yaxis.ticker = FixedTicker(ticks=np.arange(df_kde.shape[1])) # ❺
fig_tick.yaxis.ticker.tags = ["x", "y", "z"]
fig_tick.yaxis.formatter = CustomJSTickFormatter(
args=dict(labels=list(df_kde.columns)), code="return labels[tick];"
) # ❻
show(fig_tick)
インタラクティブツール#
チャートのツールバーには、JavaScriptコールバックコードを実行できる3つのツールがあります:CustomAction
、TapTool
、HoverTool
。CustomAction
ツールボタンがクリックされるとコールバックコードが実行され、TapTool
ツールを使用するとチャート内の図形をクリックしたときにコールバックコードが実行され、HoverTool
を使用するとマウスが図形上に移動したときにコールバックコードが実行されます。以下の例では、HoverTool
ツールを使用して中国の各州の概要を表示し、その効果は次のグラフのようになります。カーソルが州の上にホバーすると、右側の情報欄に州の情報がリアルタイムで表示されます。
❶ファイルchina_simple.jsonは、中国の各州の形状を保存する地理空間データ(GeoJSON)ファイルです。BokehのGeoJSONDataSource
データソースはGeoJSONテキストを直接処理できるため、ここでは直接このファイルを読み込みます。❷ファイルchina_info.jsonは各州の概要を保持するJSONファイルです。json.load()
を使用してこのファイルを読み込み、辞書オブジェクトprovince_info
を取得します。この辞書のキーは州名であり、値は概要テキストです。
❸patches()
を使用して各州の形状を表示し、塗りつぶし色はデータソースのpopulation
列を使用して計算します。❹Div
要素を使用して概要テキストを表示し、そのtags
属性を使用してすべての州の概要province_info
を保存します。Bokehは自動的にPythonの辞書オブジェクトをJavaScriptの対応するオブジェクトに変換します。❺HoverTool
ツールオブジェクトを作成します。属性renderers
はこのツールのコールバックコードをトリガーできる図形オブジェクトのリストです。
コールバックコードでは、❻cb_data.index.indices
はマウスがホバーしている図形に対応するインデックスリストです。このインデックスリストが空でない場合は、その最初のインデックスを取得し、関連情報を表示します。❼データソースでは、name
列に各州の名前が保存されており、マウスがホバーしている州の名前を取得した後、❽州の概要辞書info
で対応する概要テキストを見つけ、div_info.text
属性を更新してインターフェースに概要を表示します。
import json
from bokeh.models import GeoJSONDataSource, Div, CustomJS, HoverTool
from bokeh.layouts import row
from bokeh.transform import linear_cmap
from bokeh.palettes import Viridis
with open("data/china_simple.json", encoding="utf-8") as f: # ❶
geojson_string = f.read()
with open("data/china_info.json", encoding="utf-8") as f: # ❷
province_info = json.load(f)
source = GeoJSONDataSource(geojson=geojson_string)
fig = figure(match_aspect=True, aspect_scale=1, frame_width=500, frame_height=500)
patches = fig.patches(
"xs",
"ys",
source=source,
line_color="black", # ❸
fill_color=linear_cmap("population", Viridis[256], low=None, high=None),
)
div_info = Div(width=300, tags=[province_info]) # ❹
callback = CustomJS(
args=dict(source=source, div_info=div_info),
code="""
let indices = cb_data.index.indices; //❻
let info = div_info.tags[0];
if(indices.length > 0){
let name = source.data['name'][indices[0]]; //❼
let text = info[name]; //❽
div_info.text = `<h2>${name}</h2><p>${text}</p>`;
}
""",
)
hover = HoverTool(tooltips=None, callback=callback, renderers=[patches]) # ❺
fig.tools.append(hover)
layout = row(fig, div_info)
show(layout)