How to create a callback executed when a curve is turned off in legends

I have the next application my_app.py:

import numpy as np
import holoviews as hv
from bokeh.io import curdoc
from bokeh.layouts import layout
from bokeh.models import HoverTool
hv.extension(‘bokeh’)
renderer = hv.renderer(‘bokeh’).instance(mode=‘server’)

frequencies = [0.5, 1.0]

def sine_curve(phase, freq):
xvals = [0.1* i for i in range(100)]
return hv.Curve((xvals, [np.sin(phase+freq*x) for x in xvals]))

def callback1():
print(‘Curve “0.5” is turn off’)
def callback2():
print(‘Curve “0.5” is turn on’)
curve_dict = {f:sine_curve(0,f) for f in frequencies}

ndoverlay = hv.NdOverlay(curve_dict, kdims=‘frequency’)
doc=curdoc()
hvplot = renderer.get_plot(ndoverlay, doc)
plot=layout(hvplot.state)
doc.add_root(plot)

I need to make sure that when you turn off the curve “0.5” in the legend, the callback1 () function is called, and when you turn on the curve “0.5” callback2 () function is called.
How to implement this?

I don’t know if it’s a bokeh or a holoview question but
something like that should do the trick:

import numpy as np
import holoviews as hv
from bokeh.io import curdoc
from bokeh.layouts import layout
from bokeh.models import HoverTool
hv.extension('bokeh')
renderer = hv.renderer('bokeh').instance(mode='server')

frequencies = [0.5, 1.0]
def callback(attr,old,new):
    if new:
        print('Curve “0.5” is turn off')
    else:
        print('Curve “0.5” is turn on')
    
def sine_curve(phase, freq):
    xvals = [0.1* i for i in range(100)]
    return hv.Curve((xvals, [np.sin(phase+freq*x) for x in xvals]))

curve_dict = {f:sine_curve(0,f) for f in frequencies}

ndoverlay = hv.NdOverlay(curve_dict, kdims='frequency')
doc=curdoc()
hvplot = renderer.get_plot(ndoverlay, doc)
legend_items = hvplot.handles['legend_items']
for li in legend_items:
    if li.label['value'] == '0.5':
        li.renderers[0].on_change('muted',callback)

plot=layout(hvplot.state)
doc.add_root(plot)

1 Like

Beat me to it. :smiley: Here’s my solution - a bit more generic and removes the unnecessary call to layout:

import holoviews as hv
import numpy as np

from bokeh.io import curdoc

hv.extension('bokeh')
renderer = hv.renderer('bokeh').instance(mode='server')

frequencies = [0.5, 1.0]


def sine_curve(phase, freq):
    xvals = [0.1 * i for i in range(100)]
    return hv.Curve((xvals, [np.sin(phase + freq * x) for x in xvals]), name=str(freq))


def callback1():
    print('Curve "0.5" is turn off')


def callback2():
    print('Curve "0.5" is turn on')


curve_dict = {f: sine_curve(0, f) for f in frequencies}

ndoverlay = hv.NdOverlay(curve_dict, kdims='frequency')
doc = curdoc()
plot = renderer.get_plot(ndoverlay, doc).state


def find_and_attach_to_legend(legend, label):
    for i in legend.items:
        if i.label['value'] == label:
            if legend.click_policy == 'hide':
                attr = 'visible'
                invert = False
            elif legend.click_policy == 'mute':
                attr = 'muted'
                invert = True
            else:
                raise RuntimeError('Unhandled click_policy value', legend.click_policy)

            def on_visible_change(attr, old, new):
                if invert:
                    new = not new
                if new:
                    callback2()
                else:
                    callback1()

            i.renderers[0].on_change(attr, on_visible_change)
            break


find_and_attach_to_legend(plot.legend, '0.5')

doc.add_root(plot)
1 Like