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
return self.map_plot * self.coastline * self.borders
return hv.Overlay([self.map_plot, self.coastline, self.borders])
- `(…, features=[“coastline”, “borders”])
- 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()