Custom Bokeh Toolbar Icon

Here’s an example of creating a custom Bokek tool/toolbar icon. It is toggling the visibility on a tabulator table that is intentionally overlapping a plot

import numpy as np
import holoviews as hv
import panel as pn
import pandas as pd
from bokeh.models import Panel, Node, CustomJS, CustomAction
from bokeh.models.dom import HTML
from bokeh.plotting import show
import base64
from textwrap import dedent

hv.extension('bokeh')
pn.extension('tabulator')

xs = np.linspace(0, 10, 100)
curves = {f'Curve {i}': hv.Curve((xs, np.sin(xs + i))) for i in range(40)}
overlay = hv.Overlay(curves).opts(responsive=True, min_height=400)

legend_data = pd.DataFrame({
    'Curve': list(curves.keys()),
    'Color': ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd']*8
})

legend_table = pn.widgets.Tabulator(legend_data, sizing_mode='stretch_height', theme="midnight", pagination='local', page_size=7)

attribution = Panel(
    position=Node(target="frame", symbol="top_left"),
    anchor="top_left",
    css_variables={
        "--max-width": Node(target="frame", symbol="width"),
    },
    stylesheets=["""
:host {
  padding: 2px;
  background-color: rgba(211, 211, 211, 0.7);
  font-size: 8px;
  border-radius: 5px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 2%;
  min-height: 2%;
  max-height: 85%;
}
"""],
    elements=[
        HTML(legend_table.get_root()),
    ],
    styles={"resize": "both", "overflow": "hidden"}
)

legend_icon = """
<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
  <!-- Circle and Line -->
  <circle cx="15" cy="20" r="12" stroke="black" stroke-width="3" fill="none"/>
  <line x1="35" y1="20" x2="90" y2="20" stroke="black" stroke-width="3"/>
  
  <!-- Triangle and Line -->
  <polygon points="15,60 27,84 3,84" stroke="black" stroke-width="3" fill="none"/>
  <line x1="35" y1="70" x2="90" y2="70" stroke="black" stroke-width="3"/>
</svg>

"""

encoded_icon = base64.b64encode(dedent(legend_icon).encode()).decode('ascii')

def add_legend_to_plot(plot, element):
    figure = plot.handles["plot"]
    figure.elements.append(attribution)

def add_legend_toolbar_icon(plot, element):
    figure = plot.handles["plot"]
    legend_tool = CustomAction(
        icon=f"data:image/svg+xml;base64,{encoded_icon}",
        description="Toggle Legend",
        callback=CustomJS(
            args={"attribution": attribution},
            code="""
            export default ({attribution}) => {
                attribution.visible = !attribution.visible;
            }""",
        )
    )
    figure.toolbar.tools.append(legend_tool)

overlay.opts(hooks=[add_legend_to_plot, add_legend_toolbar_icon])

overlay

image

Thanks for sharing this! I’m not able to reproduce it, though. Among other things, importing Panel from bokeh.models leads to an error in bokeh 3.3. Doing some digging, it looks like Panel was removed in bokeh 3 (or earlier/later?), possibly renamed to TabPanel. But the behavior of TabPanel is different, so a call to it using the arguments you pass to it fails; for example, position, anchor and css_variables are not valid arguments.

hv.show_versions() is a nice way of showing all holoviz package versions.

Thanks!

You need a newer Bokeh version for this to work.