How to modify hv.Layout/pn.Tabs object instead of replace it in Parameterized class upon widget interaction?

Hi everyone, I have browsed this website alot and found the answers to many of my issues, but am posting now in hopes that someone can help me or explain where I am going wrong. I am trying to create a dashboard using Holoviews, Panel and Param by way of a parameterized class. The dashboard should contain a few parameters/widgets and a holoviews Layout in a panel Tabs object that is controlled by the widgets. A reproducible example is below.

import holoviews as hv
import param
import panel as pn
import numpy as np


class viewer(param.Parameterized):
    tabs_switch = param.Boolean(True, label="Toggle Tabs")
    cmap = param.Selector(objects=['rainbow', 'fire', 'bwr'])
    
    layout = param.Parameter()
    
    def __init__(self, **params):
        super().__init__(**params)
        image = pn.Column(pn.Param(self.param, parameters=['cmap']),
                          self.create_image)
                          
        contours = pn.Column(pn.Param(self.param, parameters=['cmap']),
                             self.create_contours)
        
        self.col = pn.Column(self.make_layout)
                                                            
        self.tabs = pn.Tabs(self.layout, closable=True)   
    
        self.column = pn.Column(pn.Param(self.param, parameters=['tabs_switch', 'cmap']), self.tabs)
        
    @param.depends('cmap')
    def create_image(self):
        x,y = np.mgrid[-50:51, -50:51] * 0.05
        img = hv.Image(np.sin(x**2+y**3)).opts(cmap=self.cmap)
        return img

    @param.depends('cmap')
    def create_contours(self):
        img = self.create_image()
        contours = hv.operation.contours(img, levels=5).opts(cmap=self.cmap)
        return contours
    
    @param.depends('tabs_switch', 'cmap')
    def make_layout(self):
        image = self.create_image()
        contours = self.create_contours()
        self.layout = hv.Layout(image + contours).opts(tabs=self.tabs_switch)
    
    
v = viewer()
v.column

When I run this, I am able to create an image, create a contours plot, put the images into a layout, and display the layout in a pn.Tabs object (which is closable). There are two parameters: cmap, which changes the colormap of the images, and tabs_switch, which toggles whether the image and contours are side-by-side or put into tabs. The issue is that the widgets are not responsive - nothing happens when changing the cmap or toggling the tabs.

I need the hv.Layout object to be inside the pn.Tabs for a couple reasons: turning tabs off inside the layout so that the plots are side-by-side with the tabs_switch parameter, and being able to close the overall Tabs object with closable=True. The final product will have several of these Tabs/Layout objects in one dashboard, so a user can add additional plots, and use the closable feature to clear them, or take the plots out of the tabs to see side-by-side. I also need the plots within the tabs to be responsive to widgets, and not in a way which completely re-returns the Tabs object, but instead modifies the plots inside. From what I understand this is the best practice and most effective in a Parameterized class. I first noticed this when changing the colormap while viewing the second tab caused the Tabs view to return to the first tab, and found this post: Panel Tabs as item in panel Column switches to first tab on widget interactions . I would really like it to stay on the current tab when using the widgets.

Does anyone know how I can make this work? I am essentially trying to use widgets to update images arranged in a Layout (which can also be made into tabs), then nested in an outer pn.Tabs. As of now, changing the colormap or toggling the tabs produces no response.

This is how I did it initially, which re runs and returns the result upon widget interaction and will reset to the first tab when the colormap changes, and from what I understand is the ineffective way of using a Parameterized class.

import holoviews as hv
import param
import panel as pn
import numpy as np


class viewer(param.Parameterized):
    tabs_switch = param.Boolean(True, label="Toggle Tabs")
    cmap = param.Selector(objects=['rainbow', 'fire', 'bwr'])
    
    layout = param.Parameter()
    
    def __init__(self, **params):
        super().__init__(**params)
        image = pn.Column(pn.Param(self.param, parameters=['tabs_switch', 'cmap']),
                          self.create_image)
                          
        contours = pn.Column(pn.Param(self.param, parameters=['tabs_switch', 'cmap']),
                             self.create_contours)
                                                                    
        self.tabs = pn.Tabs(self.make_layout, closable=True)   
    
        self.column = pn.Column(pn.Param(self.param, parameters=['tabs_switch', 'cmap']), self.tabs)
        
    @param.depends('cmap')
    def create_image(self):
        x,y = np.mgrid[-50:51, -50:51] * 0.05
        img = hv.Image(np.sin(x**2+y**3)).opts(cmap=self.cmap)
        return img

    @param.depends('cmap')
    def create_contours(self):
        img = self.create_image()
        contours = hv.operation.contours(img, levels=5).opts(cmap=self.cmap)
        return contours
    
    @param.depends('tabs_switch', 'cmap')
    def make_layout(self):
        image = self.create_image()
        contours = self.create_contours()
        layout = hv.Layout(image + contours).opts(tabs=self.tabs_switch)
        return layout
    
    
v = viewer()
v.column

Hi @veevee56

The most important part when changing to functions that don’t return anything is that you need to add watch=True.

I’ve refactored the script a bit and now it works like shown below.

import holoviews as hv
from panel.pane import plot
import param
import panel as pn
import numpy as np
from holoviews import opts

hv.extension("bokeh", "matplotlib")
pn.extension(sizing_mode="stretch_width")


class viewer(param.Parameterized):
    tabs_switch = param.Boolean(True, label="Toggle Tabs")
    cmap = param.Selector(objects=["rainbow", "fire", "bwr"])

    layout = param.Parameter()

    def __init__(self, **params):
        super().__init__(**params)

        self.plot = pn.pane.HoloViews(sizing_mode="stretch_both")
        self.column = pn.Column(
            pn.WidgetBox(pn.Param(self.param, parameters=["tabs_switch", "cmap"])), self.plot
        )

        self.make_layout()

    @param.depends("cmap")
    def create_image(self):
        x, y = np.mgrid[-50:51, -50:51] * 0.05
        img = hv.Image(np.sin(x ** 2 + y ** 3))
        img.opts(cmap=self.cmap, responsive=True, height=400)
        return img

    @param.depends("cmap")
    def create_contours(self):
        img = self.create_image()
        contours = hv.operation.contours(img, levels=5)
        contours.opts(cmap=self.cmap, responsive=True, height=400)
        return contours

    @param.depends("tabs_switch", "cmap", watch=True)
    def make_layout(self):
        print("updating")
        image = self.create_image()
        contours = self.create_contours()
        self.plot.object = hv.Layout(image + contours).opts(tabs=self.tabs_switch)


v = viewer()
v.column.servable()
2 Likes

Hi Marc,

Thank you for your help on this. I tested your code and although it’s very close to what I’m trying to do, I’m wondering if there is a way where interacting with the colormap widget doesn’t revert the tabs back to the first tab. I got close with the code below adapted from your code, but it now does not update with the toggle tabs interaction. Any other thoughts?

import holoviews as hv
from panel.pane import plot
import param
import panel as pn
import numpy as np
from holoviews import opts

hv.extension("bokeh", "matplotlib")

class viewer(param.Parameterized):
    tabs_switch = param.Boolean(True, label="Toggle Tabs")
    cmap = param.Selector(objects=["rainbow", "fire", "bwr"])

    layout = param.Parameter()

    def __init__(self, **params):
        super().__init__(**params)

        self.plot = pn.Tabs(closable=True)
        
        self.make_layout()
        
        self.column = pn.Column(
            pn.WidgetBox(pn.Param(self.param, parameters=["tabs_switch", "cmap"])), self.plot
        )
        

    @param.depends("cmap")
    def create_image(self):
        x, y = np.mgrid[-50:51, -50:51] * 0.05
        img = hv.Image(np.sin(x ** 2 + y ** 3))
        img.opts(cmap=self.cmap, responsive=True, height=400)
        return img

    @param.depends("cmap")
    def create_contours(self):
        img = self.create_image()
        contours = hv.operation.contours(img, levels=5)
        contours.opts(cmap=self.cmap, responsive=True, height=400)
        return contours

    @param.depends("tabs_switch", "cmap", watch=True)
    def make_layout(self):
        print("updating")
        image = self.create_image()
        contours = self.create_contours()
        self.plot.objects = list((hv.Layout(image + contours).opts(tabs=self.tabs_switch)))

v = viewer()
v.column.servable()

class viewer(param.Parameterized):
    tabs_switch = param.Boolean(True, label="Toggle Tabs")
    cmap = param.Selector(objects=["rainbow", "fire", "bwr"])

    layout = param.Parameter()

    def __init__(self, **params):
        super().__init__(**params)

        self.plot = pn.pane.HoloViews()
        
        self.make_layout()
        
        self.column = pn.Column(
            pn.WidgetBox(pn.Param(self.param, parameters=["tabs_switch", "cmap"])), self.plot
        )

    def create_image(self):
        x, y = np.mgrid[-50:51, -50:51] * 0.05
        img = hv.Image(np.sin(x ** 2 + y ** 3))
        img.opts(cmap=self.cmap, responsive=True, height=400)
        return img

    def create_contours(self):
        img = self.create_image()
        contours = hv.operation.contours(img, levels=5)
        contours.opts(cmap=self.cmap, responsive=True, height=400)
        return contours

    @param.depends("tabs_switch", watch=True)
    def make_layout(self):
        print("updating")
        image = self.create_image()
        contours = self.create_contours()
        self.plot.object = hv.Layout(image.apply.opts(cmap=self.param.cmap) + contours.apply.opts(cmap=self.param.cmap)).opts(tabs=self.tabs_switch)

v = viewer()
v.column.servable()
2 Likes

Thank you xavArtley! That is great, it works very well now. One question - does the self.param.cmap syntax instead of self.cmap work only with the param.Selector? I tested extending this method to another Selector (successfully), then tried with a param.Boolean and param.Integer, but it doesn’t seem to respond to change for the param.Boolean or produces an error entirely for the param.Integer.

How can I use this method beyond just the object selector, like with a Boolean or Integer? Or could you explain/link me an explanation of why the self.param.cmap works? Thanks so much for your help.

Here’s what I tested to extend this method:

class viewer(param.Parameterized):
    tabs_switch = param.Boolean(True, label="Toggle Tabs")
    cmap = param.Selector(objects=["rainbow", "fire", "bwr"])
    al = param.Selector(objects=[0.80, 0.40, 0.20])
    show_grid = param.Boolean(False, label='Show grid')
    column_slider = param.Integer(bounds=(1,3), default=1)
    
    layout = param.Parameter()

    def __init__(self, **params):
        super().__init__(**params)

        self.plot = pn.pane.HoloViews()
        
        self.make_layout()
        
        self.column = pn.Column(
            pn.WidgetBox(pn.Param(self.param, parameters=["column_slider", "al", "show_grid", "tabs_switch", "cmap"])), self.plot
        )
        
    def create_image(self):
        x, y = np.mgrid[-50:51, -50:51] * 0.05
        img = hv.Image(np.sin(x ** 2 + y ** 3))
        return img

    def create_contours(self):
        img = self.create_image()
        contours = hv.operation.contours(img, levels=5)
        return contours

    @param.depends("tabs_switch", watch=True)
    def make_layout(self):
        image = self.create_image()
        contours = self.create_contours()
        self.plot.object = hv.Layout(image.apply.opts(alpha=self.param.al, cmap=self.param.cmap, show_grid=self.param.show_grid) + contours.apply.opts(alpha=self.param.al, cmap=self.param.cmap, show_grid=self.param.show_grid)).opts(tabs=self.tabs_switch).cols(self.column_slider)
#self.param.show_grid unresponsive 
#change self.column_slider to self.param.column_slider to produce error
#add "column_slider" to param.depends to see it work correctly w/ self.column_slider

v = viewer()
v.column.servable()