Search code examples
pythonpython-3.xplotbokeh

How do I set default / active tools for a bokeh gridplot?


If one wants to define the active (default) tools for a bokeh plot, it can be set by passing "active_drag", "active_inspect", ... parameters to the figure instance as documented here.

I have not yet succeeded to set the standard-active tools for a gridplot, in which all single plots share a toolbar. This the relevant part of my code:

    tools = [
            PanTool(),
            BoxZoomTool(),
            WheelZoomTool(),
            UndoTool(),
            RedoTool(),
            ResetTool(),
            SaveTool(),
            HoverTool(tooltips=[
                ("Value", "$y")
                ])
            ]

    x_axes_range = Range1d(self.data.index[0], self.data.index[-1])

    for plot_type, plot_settings in pcfg.plot_types[self.name].items():

        plots.append(figure(x_axis_type="datetime", title=plot_type, plot_height = 400, x_range = x_axes_range, 
                            tools = tools, active_drag = None, active_inspect = None, active_scroll = None, active_tap = None))
    ...

    plots[plot_counter].line(self.data.index, self.data[parameter], color=parameter_settings[1], legend=parameter_settings[0])

...

gp = gridplot(plots, ncols = 1, sizing_mode = "scale_width")

script, div = components(gp)

So what happens is that the "BoxZoomTool()" is selected as active tool on the website I display it on, although I set the active tools to None in the figure initializations, but the available tools are those I passed to the figure() inits.

I did see the "toolbar_option" here, but I do not see how I can change the active tools using that parameter.


Solution

  • Update 6/4/2020

    Well, it seems that somebody created a GitHub Issue and the changes were already merged. Let's see if this is working in the next Bokeh Version


    Old Answer

    Research

    I believe you only can change the logo with toolbar_options like this:

    toolbar_options=dict(logo='gray')
    

    I hope there will more options in the future.

    I have checked how to achieve what you want (I needed to do it as well) and it seems that the gridplot uses an special toolbar to join all the plot toolbars together: a ProxyToolbar

    # Make the grid
    tools = []
    rows = []
    
    for row in children:
        row_tools = []
        row_children = []
        for item in row:
            if merge_tools:
                if item is not None:
                    for plot in item.select(dict(type=Plot)):
                        row_tools = row_tools + plot.toolbar.tools
                        plot.toolbar_location = None
            if item is None:
                width, height = 0, 0
                for neighbor in row:
                    if isinstance(neighbor, Plot):
                        width = neighbor.plot_width
                        height = neighbor.plot_height
                        break
                item = Spacer(width=width, height=height)
            if isinstance(item, LayoutDOM):
                item.sizing_mode = sizing_mode
                if isinstance(item, Plot):
                    if plot_width:
                        item.plot_width = plot_width
                    if plot_height:
                        item.plot_height = plot_height
                row_children.append(item)
            else:
                raise ValueError("Only LayoutDOM items can be inserted into Grid")
        tools = tools + row_tools
        rows.append(Row(children=row_children, sizing_mode=sizing_mode))
    
    grid = Column(children=rows, sizing_mode=sizing_mode)
    
    if not merge_tools:
        return grid
    
    if toolbar_location:
        proxy = ProxyToolbar(tools=tools, **toolbar_options)
        toolbar = ToolbarBox(toolbar=proxy, toolbar_location=toolbar_location)
    

    The tools are gathered in a list to assign them to the special toolbar. I don´t see that the default active elements are collected anywhere.

    Alternative solution

    So what you could do is to create a toolbar and the gridplot manually, where you can set the attributes you want to the Toolbar class. Check this example I have built:

    from bokeh.models import Button, ColumnDataSource, Range1d, Toolbar, ToolbarBox
    from bokeh.models.tools import HoverTool, WheelZoomTool, PanTool, CrosshairTool
    from bokeh.layouts import layout
    from bokeh.plotting import curdoc, figure
    
    x_range = Range1d(start=0, end=10)
    y_range = Range1d(start=0, end=10)
    
    # ------------------- PLOT 1 --------------------------- #
    
    plot_1 = figure(
        title='First figure',
        width=400,
        height=400,
        x_range=x_range,
        y_range=y_range,
        toolbar_location=None,
        x_axis_label='x axis',
        y_axis_label='y axis',
    )
    
    x = [1, 2, 3, 4]
    y = [4, 3, 2, 1]
    
    source = ColumnDataSource(data=dict(x=x, y=y))
    
    plot_1.circle(
        x='x',
        y='y',
        source=source,
        radius=0.5,
        fill_alpha=0.6,
        fill_color='green',
        line_color='black',
    )
    
    # ------------------- PLOT 2 --------------------------- #
    
    plot_2 = figure(
        name='plot_2',
        title='Second figure',
        width=400,
        height=400,
        x_range=x_range,
        y_range=y_range,
        toolbar_location=None,
        x_axis_label='x axis',
        y_axis_label='y axis',
    )
    
    plot_2.circle(
        x='x',
        y='y',
        source=source,
        radius=0.5,
        fill_alpha=0.6,
        fill_color='red',
        line_color='black',
    )
    
    # ---------------- ADD TOOLS TO THE PLOT --------------------- #
    
    wheel_zoom = WheelZoomTool()
    pan_tool = PanTool()
    hover = HoverTool()
    crosshair = CrosshairTool()
    tools = (wheel_zoom, pan_tool, hover, crosshair)
    
    toolbar = Toolbar(
        tools=[wheel_zoom, pan_tool, hover, crosshair],
        active_inspect=[crosshair],
        # active_drag =                         # here you can assign the defaults
        # active_scroll =                       # wheel_zoom sometimes is not working if it is set here
        # active_tap 
    )
    
    toolbar_box = ToolbarBox(
        toolbar=toolbar,
        toolbar_location='left'
    )
    
    plot_1.add_tools(*tools)
    plot_2.add_tools(*tools)
    
    # ----------------- PLOT LAYOUT -------------------------- #
    
    layout_1 = layout(
        children=[
            [toolbar_box, plot_1, plot_2],
        ],
        sizing_mode='fixed',
    )
    
    curdoc().add_root(layout_1)
    

    Note: I am doing some tests and sometimes it is not working well. The tools are marked as default but randomly don´t work, I am afraid it has something to do with the JavaScript and asyncronous tasks. So maybe we should wait.

    Second Alternative Solution

    I think I found a solution that always work. This is a kind of workaround. In my example I use two plots, but only the toolbar of the first plot is shown. Anyway you need to set the default toolbar values to both plots.

    from bokeh.models import Button, ColumnDataSource, Range1d, Toolbar, ToolbarBox
    from bokeh.models.tools import HoverTool, WheelZoomTool, PanTool, CrosshairTool, LassoSelectTool
    from bokeh.layouts import layout
    from bokeh.plotting import curdoc, figure
    
    x_range = Range1d(start=0, end=10)
    y_range = Range1d(start=0, end=10)
    
    # ------------------- PLOT 1 --------------------------- #
    
    plot_1 = figure(
        title='First figure',
        width=400,
        height=400,
        x_range=x_range,
        y_range=y_range,
        toolbar_location='left',        # show only the toolbar of the first plot
        tools='',
        x_axis_label='x axis',
        y_axis_label='y axis',
    )
    
    x = [1, 2, 3, 4]
    y = [4, 3, 2, 1]
    
    source = ColumnDataSource(data=dict(x=x, y=y))
    
    plot_1.circle(
        x='x',
        y='y',
        source=source,
        radius=0.5,
        fill_alpha=0.6,
        fill_color='green',
        line_color='black',
    )
    
    # ------------------- PLOT 2 --------------------------- #
    
    plot_2 = figure(
        name='plot_2',
        title='Second figure',
        width=400,
        height=400,
        x_range=x_range,
        y_range=y_range,
        toolbar_location=None,
        tools='',
        x_axis_label='x axis',
        y_axis_label='y axis',
    )
    
    plot_2.circle(
        x='x',
        y='y',
        source=source,
        radius=0.5,
        fill_alpha=0.6,
        fill_color='red',
        line_color='black',
    )
    
    # ---------------- ADD TOOLS TO THE PLOT --------------------- #
    
    wheel_zoom = WheelZoomTool()
    lasso_select = LassoSelectTool()
    pan_tool = PanTool()
    hover = HoverTool()
    crosshair = CrosshairTool()
    tools = (wheel_zoom, lasso_select, pan_tool, hover, crosshair)
    
    plot_1.add_tools(*tools)
    plot_2.add_tools(*tools)
    
    plot_1.toolbar.active_inspect=[crosshair]     # defaults added to the first plot
    plot_1.toolbar.active_scroll=wheel_zoom
    plot_1.toolbar.active_tap=None
    plot_1.toolbar.active_drag=lasso_select
    
    plot_2.toolbar.active_inspect=[crosshair]     # defaults added to the second plot
    plot_2.toolbar.active_scroll=wheel_zoom
    plot_2.toolbar.active_tap=None
    plot_2.toolbar.active_drag=lasso_select
    
    # ----------------- PLOT LAYOUT -------------------------- #
    
    layout_1 = layout(
        children=[
            [plot_1, plot_2],
        ],
        sizing_mode='fixed',
    )
    
    curdoc().add_root(layout_1)
    

    Developers Feedback

    In fact, Bryan (bokeh developer) told me on the chat

    I was going to actually answer that default tool activation and grid plots had never been considered together and was not supported yet, tho if you have found something that works for your specific use case that's probably the best possible. As you say it's not generally the case that users should have to work with toolbars directly, they are finicky for a number of reasons.