What's the best practice for linking up HoloViews streams with `Parameterized`

For widgets, there’s pn.Param and from_param.

What’s the equivalent for HoloViews streams?

Below is a single direction; tap updates the spinner widgets from param, but not the other way around. Is there an easy to link it up bidirectionally?

import param
import holoviews as hv
import panel as pn

pn.extension()

class TapTest(pn.custom.PyComponent):

    x = param.Number(default=0)
    y = param.Number(default=0)

    def __init__(self, **params):
        super().__init__(**params)

        self._tap = hv.streams.Tap()
        hv_pane = pn.pane.HoloViews()
        hv_pane.object = hv.DynamicMap(self.plot, streams=[self._tap])

        # is there something better?
        pn.bind(self._update_x_y, self._tap.param.x, self._tap.param.y, watch=True)
        self._layout = pn.Column(hv_pane, self.param.x, self.param.y)

    def plot(self, x, y):
        return hv.Points([(x, y)]).opts(tools=['tap'])

    def _update_x_y(self, x, y):
        self.x = x
        self.y = y

    def __panel__(self):
        return self._layout

TapTest()

Hi,

I think it should work with the addition of the tap.event() mehtod and directly plotting the self.x and self.y. Here the update x,y is directly within the plot function. It should also work if you keep it as in your example. Maybe you check how often these update functions are getting called because they might loop.

Best regards

import param
import holoviews as hv
import panel as pn
pn.extension()

class TapTest(pn.custom.PyComponent):
    x = param.Number(default=0)
    y = param.Number(default=0)

    def __init__(self, **params):
        super().__init__(**params)

        self._tap = hv.streams.Tap()
        hv_pane = pn.pane.HoloViews()
        hv_pane.object = hv.DynamicMap(self.plot, streams=[self._tap])
        
        pn.bind(self.update_tap, self.param.x, self.param.y, watch=True)
        
        self._layout = pn.Column(hv_pane, self.param.x, self.param.y)

    def update_tap(self, x, y):
        self._tap.event(x=x, y=y)

    def plot(self, x, y):
        if x and y:
            self.param.update(x=x, y=y)
        return hv.Points([(self.x, self.y)]).opts(tools=['tap'])

    def __panel__(self):
        return self._layout

pn.serve(TapTest())

Thank you! That does seem to make it bidirectional, but it’s a lot of work compared to from_param. I wonder if there’s a simpler solution.

This would be nice if it worked:

import param
import holoviews as hv
import panel as pn
pn.extension()

class TapTest(pn.custom.PyComponent):
    x = param.Number(default=0)
    y = param.Number(default=0)

    def __init__(self, **params):
        super().__init__(**params)

        self._tap = hv.streams.Tap()

        hv_pane = pn.pane.HoloViews()
        hv_pane.object = hv.DynamicMap(self.plot, streams=[self._tap])

        x_spinner = pn.widgets.FloatSlider.from_param(self.param.x)
        y_spinner = pn.widgets.FloatSlider.from_param(self.param.y)
        x_spinner.link(self._tap, value='x', bidirectional=True)
        y_spinner.link(self._tap, value='y', bidirectional=True)
        self._layout = pn.Column(hv_pane, x_spinner, y_spinner)


    def plot(self, x, y):
        return hv.Points([(x, y)]).opts(tools=['tap'])

    def __panel__(self):
        return self._layout

pn.serve(TapTest())

Hi,

thanks for writing an issue. As I am quite interested in working with biderectional tools, I tried few more things and came up with this. I don’t know exactly whats going on with the added callback and how it is connected to the the tab tool within the plot and I have also no idea why it does not create an infinite loop.
However, also not optimal.

import param
import panel as pn
import holoviews as hv
from holoviews.streams import Tap, Stream
from holoviews.plotting.bokeh.callbacks import TapCallback
from numbers import Number

pn.extension()
class TapBi(Tap):
    """
    The x/y-position of a tap or click in data coordinates.
    """
    x_sel = param.ClassSelector(class_=Number, default=None,)
    y_sel = param.ClassSelector(class_=Number, default=None,)
    def transform(self):
        return {'x_sel': self.x,
                'y_sel': self.y}

Stream._callbacks['bokeh'].update({TapBi: TapCallback})

class TapTest(pn.custom.PyComponent):
    tap = TapBi()
    def __init__(self, **params):
        super().__init__(**params,)
        hv_pane = pn.pane.HoloViews()
        hv_pane.object = hv.DynamicMap(self.plot, streams=[self.tap])
        self._layout = pn.Column(hv_pane, self.tap.param.x_sel, self.tap.param.y_sel)

    @param.depends('tap.x_sel', 'tap.y_sel', watch=True)
    def trigger_tap(self, **kwargs):
        print("update")
        self.tap.event(x=self.tap.x_sel, y=self.tap.y_sel)
    def plot(self,x,y, **kwargs): 
        return hv.Points([(x, y)]).opts(tools=['tap'])

    def __panel__(self):
        return self._layout

pn.serve(TapTest())

Planning on implementing this feature soon!