Share legend and click_policy

Hi,

Is it possible to plot as shown in the example below, but have 1 single legend and if the user clicks on one of the legend it hides the plot for all three figures? So link the legend between plots?

from bokeh.io import output_file, show, output_notebook
from bokeh.plotting import figure, row
import pandas as pd
import numpy as np
from bokeh.models import Legend

output_notebook()
x = list(range(11))
df = pd.DataFrame(data = {
    'x' : x,
    'y0' : x,
    'y1' : [10 - i for i in x],
    'y2' : [abs(i - 5) for i in x]})

colors = ['red', 'blue', 'yellow']

def plotter(df, colors):
    legend_it = []
    c = figure(width=400, plot_height=200, title=None)
    for i in enumerate(['y0', 'y1', 'y2']):
        s = c.circle('x', i[1], source = df, size=10, color=colors[i[0]], alpha=0.5)
        legend_it.append((i[1], [s]))
    legend = Legend(items=legend_it)
    legend.click_policy="mute"
    c.add_layout(legend, 'right')
    
    return c
c = plotter(df, colors)
k = -0.75
c_sqrt = plotter(np.sqrt(df), colors)
c_pow = plotter(np.power(df, k), colors)
show(row(c, c_sqrt, c_pow))

One idea could be the use of CustomJS and alter either muted or visible property. In the example below I use a glyphs name property to figure out which glyph that has been altered. I also define the middle plot as a reference plot if the common legend should be below the 3 plots.
In the function link_legend_glyphs I add js_on_change to each glyph in the reference plot.
(I did try to use js_link but without any success.)

from bokeh.io import output_file, save
from bokeh.plotting import figure, row, column
import pandas as pd
import numpy as np
from bokeh.models import Legend, CustomJS

output_file('shared_legend_hide.html')
x = list(range(11))
df = pd.DataFrame(data = {
    'x' : x,
    'y0' : x,
    'y1' : [10 - i for i in x],
    'y2' : [abs(i - 5) for i in x]})

colors = ['red', 'blue', 'yellow']

def plotter(df, colors, add_legend = False):
    legend_it = []
    c = figure(width=400, plot_height=200, title=None)
    for idx, i in enumerate(['y0', 'y1', 'y2']):
        s = c.circle(
            'x', i, source = df, size=10,
            color = colors[idx], alpha=0.5,
            name = colors[idx]
            )
        
        if add_legend:
            legend_it.append((i, [s]))
            legend = Legend(items=legend_it)
            #legend.click_policy="mute"
            legend.click_policy="hide"
            legend.orientation = 'horizontal'
            
    if add_legend:
        c.add_layout(legend, 'below')

    return c


def link_legend_glyphs(ref_fig, target_figs):
    cb = CustomJS(
        args = {'target_figs': target_figs},
        code = '''
            const glyph_name = cb_obj.name;
            for (const element of target_figs) {
              const r = element.select(name = glyph_name)[0];
              //r.muted = cb_obj.muted;
              r.visible = cb_obj.visible;
            }
        ''')
    for i in ref_fig.legend[0].items:
        r = i.renderers[0]
        #r.js_on_change('muted', cb)
        r.js_on_change('visible', cb)
        

c = plotter(df, colors)
k = -0.75

c_sqrt = plotter(np.sqrt(df), colors, add_legend = True)
c_pow = plotter(np.power(df, k), colors)
link_legend_glyphs(c_sqrt, [c, c_pow])

save(row(c, c_sqrt, c_pow))

Update: got the js_link to work. One can use:

def link_glyphs(ref_fig, target_fig):
    for i in colors:
        r = ref_fig.select(name = i)[0]
        t = target_fig.select(name = i)[0]
        r.js_link('muted', t, 'muted')

And call it with:

link_glyphs(c_sqrt, c)
link_glyphs(c_sqrt, c_pow)
2 Likes

That’s very clever!

I know that the ability to trigger CustomJS on clicking a legend item is not currently exposed (saw a GH issue for this at some point but can’t find it), but if you set its click policy to hide/mute, the visible/muted property of the renderer itself will change on click. So, trigger CustomJS on that property change instead! Very nice :smiley:

2 Likes