Rich Signell
Rich Signell

Reputation: 16375

Panel/Hvplot interaction when variable is changing

I'm trying to create a dashboard with two holoviews objects: a panel pn.widgets.Select object that contains a list of xarray variables, and a hvplot object that takes the selected variable on input, like this:

def hvmesh(var=None):
    mesh = ds[var].hvplot.quadmesh(x='x', y='y', rasterize=True, crs=crs, 
       width=600, height=400, groupby=list(ds[var].dims[:-2]), cmap='jet')
    return mesh

Here's what an example mesh looks like for a particular variable (one that has both time and height dimensions): enter image description here

I would like to have the map update when I select a variable from the panel widget: enter image description here I tried to do this as a dynamic map, like this:

from holoviews.streams import Params
import holoviews as hv

var_stream = Params(var_select, ['value'], rename={'value': 'var'})

mesh = hv.DynamicMap(hvmesh, streams=[var_stream])

but when I try to display the map, I get:

Exception: Nesting a DynamicMap inside a DynamicMap is not supported.

It would seem a common need to select the variable for hvplot from a panel widget. What is the best way to accomplish this with pyviz?

In case it's useful, here is my full attempt Jupyter Notebook.

Upvotes: 6

Views: 3198

Answers (2)

Rich Signell
Rich Signell

Reputation: 16375

Because the groupby changes with each variable selected, a list of variables can not be passed to hvplot. So one solution is to just recreate the plot each time a new variable is selected. This works:

import holoviews as hv
from holoviews.streams import Params

def plot(var=None, tiles=None):
    var = var or var_select.value
    tiles = tiles or map_select.value
    mesh = ds[var].hvplot.quadmesh(x='x', y='y', rasterize=True, crs=crs, title=var,
                                   width=600, height=400, groupby=list(ds[var].dims[:-2]), 
                                   cmap='jet')
    return mesh.opts(alpha=0.7) * tiles

def on_var_select(event):
    var = event.obj.value
    col[-1] = plot(var=var)

def on_map_select(event):
    tiles = event.obj.value
    col[-1] = plot(tiles=tiles)

var_select.param.watch(on_var_select, parameter_names=['value']);
map_select.param.watch(on_map_select, parameter_names=['value']);

col = pn.Column(var_select, map_select, plot(var_select.value) * tiles)

producing: enter image description here

Here is the full notebook.

Upvotes: 4

philippjfr
philippjfr

Reputation: 4080

So there is a long answer and a short answer here. Let's start with the short answer, which is that there's no need to create a custom select widget for the data variable since hvPlot allows selecting between multiple data variables automatically, so if you change it to this:

rasterized_mesh = ds[time_vars].hvplot.quadmesh(
    x='x', y='y', z=time_vars[::-1], crs=crs, width=600, height=400, 
    groupby=list(ds[var].dims[:-2]), rasterize=True, cmap='jet')

enter image description here

You will get a DynamicMap that lets you select the non-spatial dimensions and the data variable and you can now embed that in your panel, no extra work needed. If that's all you care about stop here as we're about to get into some of the internals to hopefully deliver a better understanding.

Let us assume for a minute hvPlot did not allow selecting between data variables, what would we do then? So the main thing you have to know is that HoloViews allows chaining DynamicMaps but does not allow nesting them. This can be a bit hard to wrap your head around but we'll break the problem down into multiple steps and then see how we can achieve what we want. So what is the chain of events that would give us our plot?

  1. Select a data variable
  2. Apply a groupby over the non-spatial dimensions
  3. Apply rasterization to each QuadMesh

As you know, hvPlot takes care of steps 2. and 3. for us, so how can we inject step 1. before 2. and 3. In future we plan to add support for passing panel widgets directly into hvPlot, which means you'll be able to do it all in a single step. Since panel is still a very new project I'll be pointing out along the way how our APIs will eventually make this process trivial, but for now we'll have to stick with the relatively verbose workaround. In this case we have to rearrange the order of operations:

  1. Apply a groupby over the non-spatial dimensions
  2. Select a data variable
  3. Apply rasterization to each QuadMesh

To start with we therefore select all data variables and skip the rasterization:

meshes = ds[time_vars].hvplot.quadmesh(
    x='x', y='y', z=time_vars, crs=crs, width=600, height=400, 
    groupby=list(ds[var].dims[:-2]))

Now that we have a DynamicMap which contains all the data we might want to display we can apply the next operations. Here we will make use of the hv.util.Dynamic utility which can be used to chain operations on a DynamicMap while injecting stream values. In particular in this step we create a stream from the var_select widget which will be used to reindex the QuadMesh inside our meshes DynamicMap:

def select_var(obj, var):
    return obj.clone(vdims=[var])

var_stream = Params(var_select, ['value'], rename={'value': 'var'})
var_mesh = hv.util.Dynamic(meshes, operation=select_var, streams=[var_select])

# Note starting in hv 1.12 you'll be able to replace this with
# var_mesh = meshes.map(select_var, streams=[var_select])

# And once param 2.0 is out we intend to support
# var_mesh = meshes.map(select_var, var=var_select.param.value)

Now we have a DynamicMap which responds to changes in the widget but are not yet rasterizing it so we can apply the rasterize operation manually:

rasterized_mesh = rasterize(var_mesh).opts(cmap='jet', width=600, height=400)

Now we have a DynamicMap which is linked to the Selection widget, applies the groupby and is rasterized, which we can now embed in the panel. Another approach hinted at by @jbednar above would be to do all of it in one step by making the hvPlot call not dynamic and doing the time and height level selection manually. I won't go through that here but it also a valid (if less efficient) approach.

As I hinted at above, eventually we also intend to have all hvPlot parameters become dynamic, which means you'll be able to do something like this to link a widget value to a hvPlot keyword argument:

ds[time_vars].hvplot.quadmesh(
    x='x', y='y', z=var_select.param.value, rasterize=True, crs=crs,
    width=600, height=400, groupby=list(ds[var].dims[:-2]), cmap='jet')

Upvotes: 1

Related Questions