Tap Stream Not working with NdOverlay

Here is a minimal reproducible example of the issue.

import hvplot.xarray # v:0.10.0
import panel as pn # v:1.4.2
import cartopy.crs as ccrs # v:0.22.0
import param # v:2.1.0 
import xarray as xr # v:2023.9.0
from holoviews import DynamicMap # v:1.18.1
from holoviews.streams import Tap
from geoviews.feature import borders, coastline, lakes, ocean, rivers

pn.extension()

class HvPlotQuadmeshStreams(param.Parameterized):
    def tap_callback(self, x, y):
        return self.ds.air.sel(lon=x,lat=y, method='nearest').hvplot.line(x='time')
        
    def __init__(self, **params):
        self.ds = xr.tutorial.open_dataset('air_temperature').load()

        self.map_plot = (borders * coastline) * self.ds.hvplot.quadmesh(rasterize=True)
        self.tap_stream = Tap(source=self.map_plot, x=-75, y=45)
        self.dynamic_map = DynamicMap(self.tap_callback, streams=[self.tap_stream])

        super().__init__(**params)
        
    def view(self):
        return pn.Column(self.map_plot, self.dynamic_map)

app = HvPlotQuadmeshStreams()

pn.panel(app.view)

The issue has been brought by me here (by me again), in this issue (Mr.@philippjfr), this post (Mr.@timsta95) and many other places, but it essentially boils down to Tap stream a NdOverlay issue.

The above code works if you remove the (borders * coastline) * before the hvplot.quadmesh.

My problem is I have the simlarly structured app (code at the end) which tries to load an xarray dataset and display a clickable map that is used to create time series. I have been trying to figure out a hack to get this to work and I have for another similar app but the silent fails and my lack of understanding of how Panel-Holoviews-HvPlot interact for reactivity makes it impossible for me to debug.

Mr. @ahuang11 has posted about very simple example of the streams feature, Mr. @Hoxbro has told me this is potentially an issue with Geoviews features overlaying, Mr. @Marc asked me last time for a minimal reproducble example and I have a bunch of use cases for an app that can dynamically create clickable maps and produces (Holoviews | Matplotlib | Plotly) time series or vertical profiles or such.

Also I would love to do some code review with anyone who is advanced in Python and Panel as my colleagues are brilliant scientists who work with Fortran and can not help me with this :slight_smile: .If I get this working I will add it to my list of tutorials about HoloViz.

class ConfigViewer(param.Parameterized):
    def tap_callback(self, x, y):
        sel_dict = {"lon": x, "lat": y}
        if isinstance(get_crs(self.ds), RotatedPole):
            sel_dict = {"rlon": x, "rlat": y}
            # display(self.ds[self.var_sel.value].sel(**sel_dict, method='nearest'))
        self.time_series.clear()
        self.time_series.append(
            self.ds[self.var_sel.value]
            .sel(**sel_dict, method="nearest")
            .squeeze()
            .hvplot.line(x="forecast", dynamic=False)
        )
        return Div("""<hr style="border-top: 1px transparent; height: 1px;">""").opts(
            height=5
        )

    def __init__(self, **params):
        self.time_series = pn.Row()
        self.cmap = Select(
            name="Select Color Map", options=cvu.CMAP_OPTIONS, value="jet"
        )
        self.alpha = FloatSlider(
            name="Opacity",
            start=0.0,
            end=1.0,
            step=0.1,
            value=0.8,
            value_throttled=True,
        )
        self.model_sel = Select(
            name="Select Model",
            options=sorted(cvu._MODELS.keys()),
            value="HRDPS National",
        )
        self.cumul_sel = Select(
            name="Select Accumulation Period",
            options=["instant", "1_hour", "3_hours"],
            value="1_hour",
        )

        self.configs_list = cvu.get_config_df(
            self.model_sel.value, self.cumul_sel.value
        )

        self.var_sel = Select(
            name="Select Data Variable to plot",
            options=cvu.get_config_vars(self.configs_list),
            value=cvu.get_config_vars(self.configs_list)[0],
        )

        self.bp = cvu.get_filename_bp(self.configs_list)
        self.fsts = sorted(self.bp.glob(f"{_MR}*"))
        self.ds = cvu.get_ds(
            cvu.get_fstd2nc_args_var(
                self.configs_list,
                self.var_sel.value,
            )
        )

        self.map_plot = cvu.get_hvplot(
            self.ds, self.var_sel.value, self.cmap.value, self.alpha.value
        )
        x, y = cvu.reproject_point(49.555, -122.76, get_crs(self.ds))
        self.tap_stream = Tap(source=self.map_plot, x=x, y=y)
        self.dynamic_map = DynamicMap(self.tap_callback, streams=[self.tap_stream])

        # super().__init__(**params)

    @pn.depends("alpha.value", "cmap.value", watch=True)
    def _cmap_alpha_callback(self):
        self.map_plot = cvu.get_hvplot(
            self.ds, self.var_sel.value, self.cmap.value, self.alpha.value
        )

    @pn.depends("model_sel.value", "cumul_sel.value", watch=True)
    def _model_sel_callback(self):
        self.configs_list = cvu.get_config_df(
            self.model_sel.value, self.cumul_sel.value
        )
        self.var_sel.options = cvu.get_config_vars(self.configs_list)
        self.var_sel.value = cvu.get_config_vars(self.configs_list)[0]
        self.bp = cvu.get_filename_bp(self.configs_list)
        self.fsts = sorted(self.bp.glob(f"{_MR}*"))
        self.ds = cvu.get_ds(
            cvu.get_fstd2nc_args_var(
                self.configs_list,
                self.var_sel.value,
            )
        )
        self.map_plot = cvu.get_hvplot(
            self.ds, self.var_sel.value, self.cmap.value, self.alpha.value
        )
        x, y = cvu.reproject_point(49.555, -122.76, get_crs(self.ds))
        self.tap_stream = Tap(source=self.map_plot, x=x, y=y)
        self.dynamic_map = DynamicMap(self.tap_callback, streams=[self.tap_stream])

    @pn.depends("var_sel.value", watch=True)
    def _var_sel_callback(self):
        self.ds = cvu.get_ds(
            cvu.get_fstd2nc_args_var(
                self.configs_list,
                self.var_sel.value,
            )
        )
        self.map_plot = cvu.get_hvplot(
            self.ds, self.var_sel.value, self.cmap.value, self.alpha.value
        )
        x, y = cvu.reproject_point(49.555, -122.76, get_crs(self.ds))
        self.tap_stream = Tap(source=self.map_plot, x=x, y=y)
        self.dynamic_map = DynamicMap(self.tap_callback, streams=[self.tap_stream])

    @pn.depends("alpha.value", "cmap.value", "var_sel.value")
    def view(self):
        self.map_plot = cvu.get_hvplot(
            self.ds, self.var_sel.value, self.cmap.value, self.alpha.value
        )
        x, y = cvu.reproject_point(49.555, -122.76, get_crs(self.ds))
        self.tap_stream = Tap(source=self.map_plot, x=x, y=y)
        self.dynamic_map = DynamicMap(self.tap_callback, streams=[self.tap_stream])
        return pn.Column(self.map_plot, self.dynamic_map)

HA! I figured out a hack.

import hvplot.xarray # v:0.10.0
import panel as pn # v:1.4.2
import cartopy.crs as ccrs # v:0.22.0
import param # v:2.1.0 
import xarray as xr # v:2023.9.0
from holoviews import DynamicMap # v:1.18.1
from holoviews.streams import Tap
from geoviews.feature import borders, coastline, lakes, ocean, rivers

pn.extension()

class HvPlotQuadmeshStreams(param.Parameterized):
    def tap_callback(self, x, y):
        return self.ds.air.sel(lon=x,lat=y, method='nearest').hvplot.line(x='time')
        
    def __init__(self, **params):
        self.ds = xr.tutorial.open_dataset('air_temperature').load()

        self.map_plot = self.ds.hvplot.quadmesh(rasterize=True)
        self.tap_stream = Tap(source=self.map_plot, x=-75, y=45)
        self.dynamic_map = DynamicMap(self.tap_callback, streams=[self.tap_stream])

        super().__init__(**params)
        
    def view(self):
        return pn.Column((borders * coastline) * self.map_plot, self.dynamic_map)

app = HvPlotQuadmeshStreams()

pn.panel(app.view)

Overlaying after setting the stream (return statement of view) seems to do the trick! I would say that it should have worked either way but at least I can advance on my projects now :slight_smile: