GeoViews and Holoviews - Is it possible to make the muted data points unselectable, like Bokeh does?

I have several interactive legends for some points in a map developed using GeoViews and also for some point plots using Holoviews, but I found after I muted some of the points by clicking the corresponding legends, the points can still be selected using box-select and other select tools. This can be distracted if I just want to temporarily focus on the un-muted points. While, in Bokeh, after the points are muted, they will never be selected.

Thanks!

@happybearxp Could you provide an example of the behavior in bokeh and in HoloViews? HoloViews just renders using bokeh so the mechanism behind muting is exactly the same so I really can’t account for any differences here.

I think Bokeh has two options for “legend.click_policy”, “hide” and “mute”. If it is “hide”, the hided data points will not be selected. Not sure if Holoviews and GeoViews have a similar option for this?

See two screenshots below. Legends “B” are muted in both two plots, and only data points of “A” are intended to be selected using Box Select tool. In Holoviews plot, the red points from Legend “B” are selected, while only Legend “A” points are selected in Bokeh plot:

Here is my example code:

data_plot_opts = dict(muted_alpha = 0.,
default_tools=[‘pan’,‘box_select’,‘reset’,‘wheel_zoom’, ‘save’],
active_tools =[‘box_select’])

y1=random.sample(range(100), 20)
x1=random.sample(range(100), 20)
x2=random.sample(range(100), 20)
y2=random.sample(range(100), 20)

Holoview:

df_trial1=pd.DataFrame({‘y1’:y1, ‘x1’:x1})
df_trial2=pd.DataFrame({‘y2’:y2, ‘x2’:x2})

data_profile_1 = hv.Points(df_trial1, kdims=[‘x1’, ‘y1’], label=“A”).opts(**data_plot_opts)
data_profile_2 = hv.Points(df_trial2, kdims=[‘x2’, ‘y2’], label=“B”).opts(**data_plot_opts)
data_profile_Holoview = pn.Column((data_profile_1*data_profile_2).opts(width=400, height=400), sizing_mode=‘stretch_width’)

Bokeh:

p = figure(plot_width=400, plot_height=400)
p.circle(x1, y1, color=“navy”, alpha=0.8, legend_label=‘A’)
p.circle(x2, y2, color=“red”, alpha=0.8, legend_label=‘B’)
p.add_tools(BoxSelectTool())
p.legend.location = “top_right”
p.legend.click_policy=“hide”

data_profile_tab = pn.Tabs((‘Holowview’, data_profile_Holoview), (‘Bokeh’, p), tabs_location=‘above’)

all_sections_view = pn.Row(data_profile_tab, width_policy=‘max’, height_policy=‘max’)

layout = all_sections_view.get_root()

curdoc().add_root(layout)
curdoc().title = “Legend_Mute”

Here is the Bokeh plot:

That’s perfect thank you! We should definitely expose that option on the legend in some way, e.g. with a legend_opts option. For now you could use a hook:


data_plot_opts = dict(muted_alpha = 0., default_tools=['pan','box_select','reset','wheel_zoom', 'save'], active_tools =['box_select'])

y1=random.sample(range(100), 20)
x1=random.sample(range(100), 20)
x2=random.sample(range(100), 20)
y2=random.sample(range(100), 20)

df_trial1=pd.DataFrame({'y1':y1, 'x1':x1})
df_trial2=pd.DataFrame({'y2':y2, 'x2':x2})

def click_policy(plot, element):
    plot.state.legend.click_policy = 'hide'

data_profile_1 = hv.Points(df_trial1, kdims=['x1', 'y1'], label="A").opts(**data_plot_opts)
data_profile_2 = hv.Points(df_trial2, kdims=['x2', 'y2'], label="B").opts(**data_plot_opts)
pn.Column((data_profile_1*data_profile_2).opts(width=400, height=400, hooks=[click_policy]), sizing_mode='stretch_width')
1 Like

Thanks, Philipp. It works perfectly!

philippjfr, thank you for the hook solution. It indeed works great with holoviews. Do you think it’d possible to modify the function in order to have updated axes range after hiding a glyph. So, for example, in plot attached above, after hiding trace for ‘B’ , X axis should have range (~30, 100) and Y axis (0, 80). This’d be very handy

@tmikolajczyk It’s a bit verbose but here’s an approach with a bokeh CustomJS callback:

import random

data_plot_opts = dict(muted_alpha = 0., default_tools=['pan','box_select','reset','wheel_zoom', 'save'], active_tools =['box_select'])

y1=random.sample(range(100), 20)
x1=random.sample(range(100), 20)
x2=random.sample(range(100), 20)
y2=random.sample(range(100), 20)

df_trial1=pd.DataFrame({'y1':y1, 'x1':x1})
df_trial2=pd.DataFrame({'y2':y2, 'x2':x2})

from bokeh.models import CustomJS
def click_policy(plot, element):
    p = plot.state
    p.legend.click_policy = 'hide'
    cb = CustomJS(code="""
    if (!r1.visible) {
      var [low, high] = r2_range
    } else if (!r2.visible) {
        var [low, high] = r1_range
    } else {
      var [low, high] = total
    }
    y_range.start = low
    y_range.end = high
    """,
    args={
        'r1': p.renderers[0],
        'r2': p.renderers[1],
        'y_range': p.y_range,
        'total': [p.y_range.start, p.y_range.end],
        'r1_range': data_profile_1.range(1),
        'r2_range': data_profile_2.range(1)
    })
    p.renderers[0].js_on_change('visible', cb)
    p.renderers[1].js_on_change('visible', cb)
    

data_profile_1 = hv.Points(df_trial1, kdims=['x1', 'y1'], label="A").opts(**data_plot_opts)
data_profile_2 = hv.Points(df_trial2, kdims=['x2', 'y2'], label="B").opts(**data_plot_opts)

pn.Column((data_profile_1*data_profile_2).opts(width=400, height=400, hooks=[click_policy]), sizing_mode='stretch_width')
1 Like

Thanks again for the help! Unfortunately I doesn’t work for me (axes are not updating). I’ll try again. Anyways, I guess for an overlayed plot I should pass layers to the function and create as many ranges and renderers as I have in a plot. Am I rigth?