bokeh icon indicating copy to clipboard operation
bokeh copied to clipboard

Add support toggleable `CustomAction` tool buttons

Open spott opened this issue 1 year ago • 4 comments

Problem description

It is really hard to come up with a good legend position that works for all input data, while keeping the plot compact, so inevitably it will obscure the underlying data for some data inputs, or will take up an overly large amount of screen real estate.

However, it is frequently necessary to either hide or show items, or just to know what you are looking at.

Being able to hide/show the legend as needed would be very helpful towards making plots that work pretty well on disparate datasets without spending too much time on legend positioning.

Feature description

Add a button on the toolbar that allows you to show/hide the legend.

Potential alternatives

Putting the legend outside the plot area works, however it then takes up more real estate. Developing complex heuristics for where the legend can go and cover the least amount of data, but this can be error prone and complicated.

Allowing the legend to be dragged around is another feature request that could be valuable, but this seems simpler.

Additional information

No response

spott avatar Mar 19 '24 18:03 spott

FWIW this seems like something that could be accomplished currently with a CustomAction and one line of JS code to toggle .visible on the legend.

bryevdv avatar Mar 19 '24 18:03 bryevdv

Based on examples/basic/annotations/legend.py:

import numpy as np

from bokeh.layouts import gridplot
from bokeh.plotting import figure, show

x = np.linspace(0, 4*np.pi, 100)
y = np.sin(x)

TOOLS = "pan,wheel_zoom,box_zoom,reset,save,box_select"

p1 = figure(title="Legend Example", tools=TOOLS)

p1.scatter(x,   y, legend_label="sin(x)")
p1.scatter(x, 2*y, legend_label="2*sin(x)", color="orange")
p1.scatter(x, 3*y, legend_label="3*sin(x)", color="green")

p1.legend.title = 'Markers'

p2 = figure(title="Another Legend Example", tools=TOOLS)

p2.scatter(x, y, legend_label="sin(x)")
p2.line(x, y, legend_label="sin(x)")

p2.line(x, 2*y, legend_label="2*sin(x)",
        line_dash=(4, 4), line_color="orange", line_width=2)

p2.scatter(x, 3*y, legend_label="3*sin(x)",
           marker="square", fill_color=None, line_color="green")
p2.line(x, 3*y, legend_label="3*sin(x)", line_color="green")

p2.legend.title = 'Lines'

from bokeh.models import CustomAction, CustomJS
toggle_legend = CustomAction(
    description="Toggle legend",
    callback=CustomJS(args=dict(legends=p1.legend + p2.legend), code="""
export default ({legends}) => {
    for (const legend of legends) {
        legend.visible = !legend.visible
    }
}
"""),
)

gp = gridplot([p1, p2], ncols=2, width=400, height=400)
gp.toolbar.tools.append(toggle_legend)

show(gp)

Screencast from 19.03.2024 19:58:16.webm

Would be even better if we could use toggleable button (see PR #13571).

mattpap avatar Mar 19 '24 18:03 mattpap

Note that this has to be added to GridPlot.toolbar and not Plot.toolbar, because in the later case, due to proxying, the custom action would be called as many times as there are plots, resulting in unexpected behavior. This can be alleviated by using gridplot(plots, merge_tools=False).

mattpap avatar Mar 19 '24 19:03 mattpap

Would be even better if we could use toggleable button.

I think adding a toggle-able option for a custom action would be great.

I am less enthusiastic about adding a new built-in tool just for this.

bryevdv avatar Mar 19 '24 19:03 bryevdv