Using xarray non-dimensional coordinate as `index_cols` on linked plots?

Hey there!

I am new to HoloViews but the ecosystem seems very useful. I am trying to couple various different interactions together but it has been a difficult affair.

I have a data cube of hyperspectral images as an xarray that I want to plot as a HeatMap, using a DynamicMap to select the specific wavelength slice to display. Additionally, I want to show a Curve as a DynamicMap of the average spectrum of all selected data points.
I also have embeddings, which are basically 2 values per pixel of the data cube that I want to plot against each other using a Points plot. This has to be datashaded as there are way too many points to plot normally.

Now, I would like to use the link_selections functionality to allow selections in the HeatMap and embeddings Points plot to sync selections. I am using an xr.Dataset to store the data and would like to use an index coordinate to sync the selections easily, which I included as a non-dimensional coordinate in the xarray. I also tried linking the embeddings to my x and y coordinates but it seems that HoloViews ignores both these links when using link_selections. Is this currently not supported or might I be doing something wrong.
If I make a selection now, the selection is only applied to the data in the plot (and the selection is not even visible).

I have included a picture below of what it is supposed to look like (static mockup) in the end and I’ve included my code below but I’m not sure whether it is understandable. I understand its all quite a complex story, so I hope someone can help me.

import xarray as xr

# Get shape parameters from the data
x_len = selected_absorbance.shape[1]
y_len = selected_absorbance.shape[0]
embed_len = umap_embeddings2d["10n"].shape[1]

# Create coordinates
x = np.arange(1, x_len + 1)
y = np.arange(1, y_len + 1)
band = (np.arange(band_max, band_min+band_change, band_change)).astype(int)
idx = np.arange(y_len * x_len).reshape(x_len, y_len)

# Extract 2 components to plot 
comp1 = deepcopy(umap_embeddings2d["10n"].iloc[:, 0].to_numpy()).reshape(x_len, y_len)
comp2 = deepcopy(umap_embeddings2d["10n"].iloc[:, 1].to_numpy()).reshape(x_len, y_len)

# Create the xr dataset
xrds = xr.Dataset(
    {
        "absorbance": (["y", "x", "band"], selected_absorbance),
    },
    coords={
        "x": x,
        "y": y,
        "band": band,
        "idx": (("x", "y"), idx),
        "comp1": (("x", "y"), comp1),
        "comp2": (("x", "y"), comp2),
    }
    )

# Set metadata
xrds.absorbance.attrs = dict(
    units = "Absorbance",
    standard_name = "absorbance_maps",
    long_name = "FTIR absorbance maps for every scanned wavelength",
)

xrds.band.attrs = dict(
    standard_name = "wavenumber",
    long_name = "Wavenumber",
    units = "cm⁻¹",
    step = 2, 
)


# Dynamic heatmap class
class ImageBand(param.Parameterized):

    band = param.Integer(default=1538, bounds=(band_min, band_max), step=2)
    
    plot_options = param.Dict(dict(aspect="equal", cmap="Plasma"))
    
    @param.depends("band", "plot_options")
    def create_heatmap(self):
        
        return hv.HeatMap(xrds.sel(band=self.band), kdims=["x", "y"]).opts(**dict(title=f"Absorbance at {self.band}", **self.plot_options))


# Selection instance
sel3 = hv.selection.link_selections.instance()

band_select = ImageBand()
hm = sel3(band_select.create_heatmap().opts(tools=["box_select"]))

embed = sel3( datashade(
            hv.Points(xrds.sel(band=band), ["comp1", "comp2"], vdims=["absorbance"]),
        ) )

hv.Layout([hm, embed]).cols(1);

I’ll take a deeper look once I get a chance but as a first suggestion I’d strongly urge you to replace the HeatMap with an Image. HeatMap is primarily aimed at categorical use cases where you have very few values.

Another comment, it’s much easier to provide useful feedback if you provide a reproducible example either by providing the data files or by reducing your example to something minimal (preferable).

The last suggestion I can give without a reproducible example is replacing:

hm = sel3(band_select.create_heatmap().opts(tools=["box_select"]))

with:

hm = sel3(hv.DynamicMap(band_select.create_heatmap).opts(tools=["box_select"]))

to ensure the plot updates when the dependencies 'band' and 'plot_options' change.

Hi Philip,

thanks for looking over it. I spent some time to change it around a bit to generate random data so it can stand on itself.

I did not use Image because it did not show the selection the first time I tried it but it seems to work now.

However, my problem still remains. I would like the selection in one plot reflected in the other, preferably using the idx xr.Dataset column, however I could not find a way to do this.

Updated code:

import xarray as xr

import numpy as np
import pandas as pd

import holoviews as hv
from holoviews.operation.datashader import datashade
import param
import panel as pn

hv.extension("bokeh")

# Set shape parameters 
x_len = 100
y_len = 120

# Create coordinates
x = np.arange(1, x_len + 1)
y = np.arange(1, y_len + 1)
band = (np.arange(250)).astype(int)
idx = np.arange(y_len * x_len).reshape(x_len, y_len)

# Extract 2 components to plot 
comp1 = np.random.rand(x_len, y_len)
comp2 = np.random.rand(x_len, y_len)

# Create the xr dataset
xrds = xr.Dataset(
    {
        "absorbance": (["y", "x", "band"], np.random.rand(y_len, x_len, 250)),
    },
    coords={
        "x": x,
        "y": y,
        "band": band,
        "idx": (("x", "y"), idx),
        "comp1": (("x", "y"), comp1),
        "comp2": (("x", "y"), comp2),
    }
    )

# Set metadata
xrds.band.attrs = dict(
    standard_name = "wavenumber",
    long_name = "Wavenumber",
    units = "cm⁻¹",
    step = 2, 
)


# Create Image/HeatMap class
class ImageBand(param.Parameterized):

    band = param.Integer(default=100, bounds=(0, 250), step=1)
    
    plot_options = param.Dict(dict(aspect="equal", cmap="Plasma"))
    
    @param.depends("band", "plot_options")
    def create_heatmap(self):
        
        return hv.Image(xrds.sel(band=self.band), kdims=["x", "y"]).opts(**dict(title=f"Absorbance at {self.band}", **self.plot_options))


# Create linked selection and figures
ls = hv.selection.link_selections.instance(link_inputs=True)

band_select = ImageBand()
hm = ls(hv.DynamicMap(band_select.create_heatmap)).opts(tools=["box_select"])

embed = ls( datashade(
            hv.Points(xrds.sel(band=band), ["comp1", "comp2"], vdims=["absorbance"]),
        ) )


band_slider = pn.panel(band_select.param, parameters=["band"])

pn.Column(band_slider, hm, embed)