Setting up clim that depends on the variable

What I am trying to do is to create a dashboard that users can select the property that they want to display. Thus,

import hvplot.xarray
import xarray as xr

ds = xr.tutorial.open_dataset('air_temperature').load().sel(time='2013-06-01 12:00')
# create a new variable. In this case, the temperature anomaly
ds = ds.assign(air_anomaly = ds.air - ds.air.mean())

# create the interactive dataset
dsi = ds.interactive(loc = "left")

# the property options
property_list = ["air", "air_anomaly"]
property = pn.widgets.Select(options = property_list, width = 150)

This example works, but you have to manually set the vmin and vmax everytime you change the variable.

vmin = pn.widgets.FloatInput(name = 'vmin', width = 60)
vmax = pn.widgets.FloatInput(name = 'vmax', width = 60)

dsi[property].plot(clim = (vmin, vmax))

This example would be better, because users can still set the vmin and vmax, but their default values depend on the variable that is being plotted.

vmin = pn.widgets.FloatInput(name = 'vmin', width = 60, value = dsi[property].quantile(0.01).values.tolist())
vmax = pn.widgets.FloatInput(name = 'vmax', width = 60, value = dsi[property].quantile(0.99).values.tolist())

dsi[property].plot(clim = (vmin, vmax))

But this code returns an error:

---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[55], line 5
      2 vmin = pn.widgets.FloatInput(name = 'vmin', width = 60, value = dsi[property].quantile(0.01).values.tolist())
      3 vmax = pn.widgets.FloatInput(name = 'vmax', width = 60, value = dsi[property].quantile(0.99).values.tolist())
----> 5 dsi[property].plot(clim = (vmin, vmax))

File ~/miniforge3/envs/coringa/lib/python3.9/site-packages/hvplot/interactive.py:517, in Interactive.__call__(self, *args, **kwargs)
    515     method = type(new._transform)(new._transform, new._method, accessor=True)
    516     kwargs = dict(new._inherit_kwargs, **kwargs)
--> 517     clone = new._clone(method(*args, **kwargs), plot=new._method == 'plot')
    518 finally:
    519     # If an error occurs reset _method anyway so that, e.g. the next
    520     # attempt in a Notebook, is set appropriately.
    521     new._method = None

File ~/miniforge3/envs/coringa/lib/python3.9/site-packages/hvplot/interactive.py:388, in Interactive._clone(self, transform, plot, loc, center, dmap, copy, max_rows, **kwargs)
    386 else:
    387     kwargs = dict(self._inherit_kwargs, **dict(self._kwargs, **kwargs))
--> 388 return type(self)(self._obj, fn=self._fn, transform=transform, plot=plot, depth=depth,
    389                  loc=loc, center=center, dmap=dmap, _shared_obj=self._shared_obj,
    390                  max_rows=max_rows, **kwargs)

File ~/miniforge3/envs/coringa/lib/python3.9/site-packages/hvplot/interactive.py:282, in Interactive.__init__(self, obj, transform, fn, plot, depth, loc, center, dmap, inherit_kwargs, max_rows, method, _shared_obj, _current, **kwargs)
    280     self._current_ = _current
    281 else:
--> 282     self._current_ = self._transform.apply(ds, keep_index=True, compute=False)
    283 self._init = True
    284 self._dirty = False

File ~/miniforge3/envs/coringa/lib/python3.9/site-packages/holoviews/util/transform.py:767, in dim.apply(self, dataset, flat, expanded, ranges, all_values, keep_index, compute, strict)
    762     data = dataset.interface.values(
    763         dataset, lookup, expanded=expanded, flat=flat,
    764         compute=compute_for_compute, keep_index=keep_index_for_compute
    765     )
    766 for op in self.ops:
--> 767     fn, fn_name, args, kwargs, accessor = self._resolve_op(
    768         op, dataset, data, flat, expanded, ranges, all_values,
    769         keep_index_for_compute, compute_for_compute, strict
    770     )
    771     drange = ranges.get(eldim, {})
    772     drange = drange.get('combined', drange)

File ~/miniforge3/envs/coringa/lib/python3.9/site-packages/holoviews/util/transform.py:643, in dim._resolve_op(self, op, dataset, data, flat, expanded, ranges, all_values, keep_index, compute, strict)
    638     if isinstance(v, dim):
    639         v = v.apply(
    640             dataset, flat, expanded, ranges, all_values,
    641             keep_index, compute, strict
    642         )
--> 643     fn_kwargs[k] = resolve_dependent_value(v)
    644 args = tuple(fn_args[::-1] if op['reverse'] else fn_args)
    645 kwargs = dict(fn_kwargs)

File ~/miniforge3/envs/coringa/lib/python3.9/site-packages/holoviews/core/util.py:1616, in resolve_dependent_value(value)
   1614     value = [resolve_dependent_value(v) for v in value]
   1615 elif isinstance(value, tuple):
-> 1616     value = tuple(resolve_dependent_value(v) for v in value)
   1617 elif isinstance(value, dict):
   1618     value = {
   1619         resolve_dependent_value(k): resolve_dependent_value(v) for k, v in value.items()
   1620     }

File ~/miniforge3/envs/coringa/lib/python3.9/site-packages/holoviews/core/util.py:1616, in <genexpr>(.0)
   1614     value = [resolve_dependent_value(v) for v in value]
   1615 elif isinstance(value, tuple):
-> 1616     value = tuple(resolve_dependent_value(v) for v in value)
   1617 elif isinstance(value, dict):
   1618     value = {
   1619         resolve_dependent_value(k): resolve_dependent_value(v) for k, v in value.items()
   1620     }

File ~/miniforge3/envs/coringa/lib/python3.9/site-packages/holoviews/core/util.py:1640, in resolve_dependent_value(value)
   1638     value = value()
   1639 elif isinstance(value, param.Parameter) and isinstance(value.owner, param.Parameterized):
-> 1640     value = getattr(value.owner, value.name)
   1641 elif isinstance(value, FunctionType) and hasattr(value, '_dinfo'):
   1642     deps = value._dinfo

File ~/miniforge3/envs/coringa/lib/python3.9/site-packages/param/__init__.py:842, in Number.__get__(self, obj, objtype)
    837 def __get__(self, obj, objtype):
    838     """
    839     Same as the superclass's __get__, but if the value was
    840     dynamically generated, check the bounds.
    841     """
--> 842     result = super(Number, self).__get__(obj, objtype)
    843     # Should be able to optimize this commonly used method by
    844     # avoiding extra lookups (e.g. _value_is_dynamic() is also
    845     # looking up 'result' - should just pass it in).
    846     if self._value_is_dynamic(obj, objtype):

File ~/miniforge3/envs/coringa/lib/python3.9/site-packages/param/__init__.py:613, in Dynamic.__get__(self, obj, objtype)
    611     return gen
    612 else:
--> 613     return self._produce_value(gen)

File ~/miniforge3/envs/coringa/lib/python3.9/site-packages/param/__init__.py:651, in Dynamic._produce_value(self, gen, force)
    648     time_fn = self.time_fn
    650 if (time_fn is None) or (not self.time_dependent):
--> 651     value = produce_value(gen)
    652     gen._Dynamic_last = value
    653 else:

File ~/miniforge3/envs/coringa/lib/python3.9/site-packages/param/__init__.py:75, in produce_value(value_obj)
     69 """
     70 A helper function that produces an actual parameter from a stored
     71 object: if the object is callable, call it, otherwise return the
     72 object.
     73 """
     74 if callable(value_obj):
---> 75     return value_obj()
     76 else:
     77     return value_obj

File ~/miniforge3/envs/coringa/lib/python3.9/site-packages/hvplot/interactive.py:508, in Interactive.__call__(self, *args, **kwargs)
    506         return self._clone(*args, **kwargs)
    507     # TODO: When is this error raised?
--> 508     raise AttributeError
    509 elif self._method == 'plot':
    510     # This - {ax: get_ax} - is passed as kwargs to the plot method in
    511     # the dim expression.
    512     kwargs['ax'] = self._get_ax_fn()

AttributeError: 

Any cool idea on how to do that?

Here’s one way to do it I think, but maybe it’s not what you’re asking for since the following does not involve interactive.

import hvplot.xarray
import xarray as xr
import panel as pn

pn.extension()

ds = xr.tutorial.open_dataset("air_temperature").load().sel(time="2013-06-01 12:00")
# create a new variable. In this case, the temperature anomaly
ds = ds.assign(air_anomaly=ds.air - ds.air.mean())
vmin = ds["air"].quantile(0.01).values.tolist()
vmax = ds["air"].quantile(0.99).values.tolist()
clim = pn.widgets.RangeSlider(
    name="clim",
    width=60,
    value=(vmin, vmax),
    start=vmin,
    end=vmax,
)
pn.Column(
    clim,
    ds.hvplot("lon", "lat").apply.opts(clim=clim.param.value),
)

Thanks. The way you did doesn’t really update for each property, right? is there a way to make it work?

The best I could do is similar to what you do, but updates the vmin and vmax for each property. The problem is that I couldn’t make it work for the user still being able to change the values.

import hvplot.xarray
import xarray as xr
import panel as pn

ds = xr.tutorial.open_dataset('air_temperature').load().sel(time='2013-06-01 12:00')
# create a new variable. In this case, the temperature anomaly
ds = ds.assign(air_anomaly = ds.air - ds.air.mean())

# create the interactive dataset
dsi = ds.interactive(loc = "left")

# the property options
property_list = ["air", "air_anomaly"]
property = pn.widgets.Select(options = property_list, width = 150)

vmin = dsi[property].quantile(0.01)
vmax = dsi[property].quantile(0.99)

plot = dsi[property].hvplot(clim = (vmin, vmax)).opts(framewise = False) #framewise = False avoids reseting zoom with changes

layout = pn.Row(pn.Column(property, qmin, qmax), plot)

server = pn.panel(layout).show()

Another thing I tried was to set options for the percentiles we want to select for min and max. Which works oddly, because it only updates the plot when I change the property.

import hvplot.xarray
import xarray as xr
import panel as pn

ds = xr.tutorial.open_dataset('air_temperature').load().sel(time='2013-06-01 12:00')
# create a new variable. In this case, the temperature anomaly
ds = ds.assign(air_anomaly = ds.air - ds.air.mean())

# create the interactive dataset
dsi = ds.interactive(loc = "left")

# the property options
property_list = ["air", "air_anomaly"]
property = pn.widgets.Select(options = property_list, width = 150)

qmin = pn.widgets.FloatInput(name = 'scale', width = 70, value = 0.01)
qmax = pn.widgets.FloatInput(name = 'scale', width = 70, value = 0.99)

vmin = dsi[property].quantile(qmin)
vmax = dsi[property].quantile(qmax)

plot = dsi[property].hvplot(clim = (vmin, vmax)).opts(framewise = False) #framewise = False avoids reseting zoom with changes

# had to explicitly add the qmin and qmax otherwise it is not showing
layout = pn.Row(pn.Column(property, qmin, qmax), plot)

server = pn.panel(layout).show()

ezgif.com-video-to-gif (3)