Dynamic Tap Stream with Overlays

I am stuck on a weird Overlay-TapStream interaction.

The get_map_plot method returns the map which serves as a TapStream source but neither

  1. return self.map_plot * self.coastline * self.borders
  2. return hv.Overlay([self.map_plot, self.coastline, self.borders])
  3. `(…, features=[“coastline”, “borders”])
  4. Dozen of other approaches

Work and essentially produce an infinite loading indicator at best and crash the app at worst.

class CMDIValidationApp(pn.viewable.Viewer):
    ds = param.Parameter(default=None)
    ds_zarr = create_coherent_dataset(zarrs[0])
    diff = param.Parameter(default=None)

    borders = gvf.borders
    coastline = gvf.coastline

    def create_time_series_title(self):
        """Create title for time series plot."""
        if self.ds is None or self.data_var.value is None:
            return "Time Series"
        return (
            f"{self.data_var.value} at Coordinates: "
            f"lon = {self.tap_stream.x:.2f}, lat = {self.tap_stream.y:.2f}"
        )

    def create_map_title(self):
        """Create title for map plot."""
        if self.ds is None or self.data_var.value is None:
            return "Map View"
        var_attrs = self.ds[self.data_var.value].attrs
        units = var_attrs.get("units", "")
        title = f"NetCDF experiment for {self.data_var.value}"
        if units:
            title += f" [{units}]"
        return title

    def create_diff_map_title(self):
        """Create title for map plot."""
        if self.diff is None or self.data_var.value is None:
            return "Map View"
        var_attrs = self.diff.attrs
        units = var_attrs.get("units", "")
        title = f"Difference between the Zarr control and the NetCDF experiment for {self.data_var.value}"
        if units:
            title += f" [{units}]"
        return title

    def tap_stream_callback(self, x, y):
        """Callback for map tap to create time series."""
        if self.ds is None or self.data_var.value is None:
            return hv.Curve([]).opts(title="No data loaded")

        pn.state.notifications.success(
            f"Plotting {self.data_var.value} at ({x:.2f}, {y:.2f}).", duration=2000
        )

        try:
            return (
                self.ds_zarr_aligned[self.data_var.value].sel(lon=x, lat=y, method="nearest")
                .squeeze()
                .hvplot.line(title=self.create_time_series_title(), responsive=True, label="Control Zarr")
                .opts(min_height=250, max_height=400)
                * self.ds_aligned[self.data_var.value].sel(lon=x, lat=y, method="nearest")
                .squeeze()
                .hvplot.line(label="NetCDF Experiment")
            )
        except Exception as e:
            return hv.Curve([]).opts(title=f"Error: {str(e)}")

    def __init__(self, zarr_files, nc_files, **params):
        self.nc_files = nc_files

        self.file_selector = pn.widgets.Select(
            name="Select File",
            options=self._get_file_options(),
            value=self._get_file_options()[0] if self._get_file_options() else None,
        )

        self.data_var = pn.widgets.Select(name="Data Variable", options=[], value=None)

        self.cmap = pn.widgets.ColorMap(
            name="Colormap",
            options=cc.palette,
            value=cc.palette.diverging_bwr_20_95_c54,
        )

        self.load_button = pn.widgets.Button(name="Load Dataset", button_type="success")
        self.load_button.on_click(self._trigger_load_data)

        self.status_text = pn.pane.Markdown("")

        self.tap_stream = hv.streams.Tap(source=None, x=0, y=0)
        # Call super().__init__ after all attributes are set
        super().__init__(**params)

    def _get_file_options(self):
        return [f.name for f in self.nc_files]

    def _get_selected_file_path(self):
        try:
            for f in self.nc_files:
                if f.name == self.file_selector.value:
                    return f
        except:
            return None

    def _update_file_options(self, event):
        self.file_selector.options = self._get_file_options()
        if self._get_file_options():
            self.file_selector.value = self._get_file_options()[0]
        else:
            self.file_selector.value = None

    def _trigger_load_data(self, event):
        if not self.file_selector.value:
            self.status_text.object = "❌ Please select a file first."
            return

        self.load_button.loading = True
        self.load_button.disabled = True
        self.status_text.object = f"🔄 Loading {self.file_selector.value}..."

        try:
            file_path = self._get_selected_file_path()
            if file_path is None:
                raise ValueError("Selected file not found")

            self.ds = create_coherent_dataset(file_path)

            if self.ds is not None:
                data_vars = list(self.ds.data_vars.keys())
                self.data_var.options = data_vars

                self.status_text.object = f"✅ Successfully loaded {self.file_selector.value} with {len(data_vars)} variables."
            else:
                self.status_text.object = "❌ Failed to load dataset."

        except Exception as e:
            self.status_text.object = f"❌ Error loading dataset: {str(e)}"
            self.ds = None
        finally:
            self.load_button.loading = False
            self.load_button.disabled = False

    @param.depends("ds", "data_var.value", "cmap.value")
    def get_map_plot(self):
        """Create the map visualization."""
        if self.ds is None:
            return pn.pane.Markdown(
                "No data loaded yet. Select a file and click 'Load Dataset'."
            )
        elif self.data_var.value is None:
            return pn.pane.Markdown("No data variable selected.")
        elif self.data_var.value not in self.ds:
            return pn.pane.Markdown(
                f"Selected variable '{self.data_var.value}' not found in the loaded dataset."
            )
        else:
            try:
                pn.state.notifications.info(
                    f"Plotting {self.data_var.value}...", duration=2000
                )

                self.ds_aligned, self.ds_zarr_aligned, self.diff = align_and_diff(
                    self.ds, self.ds_zarr, self.data_var.value
                )

                # Create map plot
                self.map_plot = (
                    self.ds[self.data_var.value]
                    .hvplot.image(
                        datashade=True,
                        fontscale=1.5,
                        cmap=self.cmap.value,
                        geo=True,
                        crs=ccrs.PlateCarree(),
                        projection=ccrs.PlateCarree(),
                        project=True,
                        responsive=True,
                        alpha=0.7,
                        min_height=600,
                        data_aspect=1,
                        title=self.create_map_title(),
                        # features=["coastline", "borders"]
                    )
                    .opts(active_tools=["wheel_zoom"])
                )

                # Set up tap stream for time series
                if hasattr(self, "tap_stream") and self.tap_stream:
                    self.tap_stream.source = self.map_plot
                else:
                    print("get_map_plot else")
                    self.tap_stream = hv.streams.Tap(source=self.map_plot, x=0, y=0)

                if (
                    not hasattr(self, "tap_stream_dynamic_map")
                    or self.tap_stream_dynamic_map is None
                ):
                    self.tap_stream_dynamic_map = hv.DynamicMap(
                        self.tap_stream_callback, streams=[self.tap_stream]
                    )

                pn.state.notifications.success(
                    f"Successfully plotted {self.data_var.value}.",
                    duration=2000,
                )

                # Add map features
                return self.map_plot
                # return hv.Overlay([self.map_plot, borders, coastline])

            except Exception as e:
                return pn.pane.Markdown(f"Error displaying data variable: {e}")

    @param.depends("tap_stream.x", "tap_stream.y")
    def time_series_plot(self):
        """Create time series plot from tap stream."""
        if hasattr(self, "tap_stream_dynamic_map"):
            return self.tap_stream_dynamic_map
        else:
            return hv.Curve([]).opts(title="Click on map to see time series")

    @param.depends("diff")
    def diff_plot(self):
        try:
            pn.state.notifications.info(
                f"Plotting Diff Plot for {self.data_var.value}...", duration=2000
            )
            return (
                self.diff.hvplot.image(
                    datashade=True,
                    fontscale=1.5,
                    cmap=self.cmap.value,
                    geo=True,
                    crs=ccrs.PlateCarree(),
                    projection=ccrs.PlateCarree(),
                    project=True,
                    responsive=True,
                    alpha=0.7,
                    min_height=600,
                    data_aspect=1,
                    title=self.create_diff_map_title(),
                ).opts(active_tools=["wheel_zoom"])
                * self.borders
                * self.coastline
            )
        except:
            return None

    def __panel__(self):
        return pn.template.MaterialTemplate(
            header_background="#FFA500",
            logo="https://www.canada.ca/etc/designs/canada/wet-boew/assets/sig-blk-en.svg",
            site="CMDI Validation",
            title="CMDI Dataset Validation App",
            sidebar=[
                pn.WidgetBox(
                    # self.file_type,
                    self.file_selector,
                    self.load_button,
                    self.data_var,
                    self.cmap,
                    self.status_text,
                ),
                pn.panel(self.time_series_plot),
                create_file_tabs(bashs),
                create_file_tabs(yamls),
            ],
            main=[
                pn.panel(
                    self.get_map_plot,
                    widget_location="top",
                    sizing_mode="stretch_width",
                    min_height=400,
                ),
                pn.panel(
                    self.diff_plot,
                    widget_location="top",
                    sizing_mode="stretch_width",
                    min_height=400,
                ),
            ],
            sidebar_width=700,
        )

CMDIValidationApp(zarrs, ncs).servable()

I think you might need to reduce the example down to a minimum. It’s quite hard to follow given the length.

Also, try not to work with overlays with streams; one element per dynamicmap work best.
https://pydeas.readthedocs.io/en/latest/holoviz_interactions/tips_and_tricks.html#One-element-per-DynamicMap-for-flexibility