How to create a self-contained, custom "Panel"?

I’d like to create a custom class that takes a dataframe as input and returns a Panel with a dropdown populated by column names and a Matplotlib plot for the selected column. Possibly related to #1072. What’s the best way to accomplish this? I’ve been poring over the documentation for hours and I still haven’t been able to figure it out.

I started with using the interact function, like so:

from sklearn.datasets import load_wine
import seaborn as sns
import pandas as pd
import matplotlib.pyplot as plt
import panel as pn
import param
from panel.interact import interact

def histogram(column):
    ax = sns.distplot(df[column])
    plt.close()
    return pn.pane.Matplotlib(ax.figure)

class MyCustomWidget(pn.pane.base.PaneBase):
    def __init__(self, data):
        pn.extension()
        pn.config.embed = True
        self._data = data
        self._dropdown = pn.widgets.Select(options=list(data.columns.values))
        self.layout = interact(histogram, column=self._dropdown)
        super(pn.pane.base.PaneBase, self).__init__()

This allows me to instantiate the class and immediately return the visualization in a notebook:

df = load_wine(as_frame=True).data
MyCustomWidget(df) # Displays in notebook

The trade-off here is that I can’t place histogram in the class, since interact doesn’t support methods.

After reading the docs, my impression was that I should use Param instead:

class MyCustomWidget(param.Parameterized):
    column = param.Selector()

    def __init__(self, data):
        self.param.column.objects = data.columns.values

    @param.depends("column")
    def plot(self):
        if self.column:
            ax = sns.distplot(df[self.column])
            plt.close()
            return pn.pane.Matplotlib(ax.figure)

My issue here is that it seems I still have to wrap the instance object in a Panel:

d = MyCustomWidget(df) # No display
pn.Column(d.param, d.plot) 

Is there a better way to do this so that I can simply call MyCustomWidget(df) and where all of the logic is contained inside the class?

1 Like

Hi @haishiro

This is a really, really good and natural question. I’ve been wanting to create my own library of Panel extensions since I started using Panel and also contribute some that I felt where general purpose.

One way to do it is the advanced way creating Bokeh https://docs.bokeh.org/en/latest/docs/user_guide/extensions.html. That is actually how most of the layouts, panes and widgets in Panel are built. BUT THAT IS NOT APPROPRIATE FOR YOUR USE CASE.

You want to combine the existing components of Panel into a new component. And that should be the most frequent used way to create extensions to Panel.

But as far as I know this is not really described anywhere as all the examples focus on showing how to create a small dashboard or app. And I don’t remember seeing any suggestions in the different Channels (Gitter, Github, Discourse).

I think this is fundamental question to answer. I hope the community and the core developers will join the discussion and we can have an answer.

Below I will provide my take.

ps. Your question comes just at the right time as I started working on https://github.com/marcskovmadsen/panel-extensions-template the same day. I also announced it on Discourse here How to create Panel Extensions?. I will use your question and example as inspiration for improvements.

pps. Looking back I actually created a discussion/ feature request for this https://github.com/holoviz/panel/issues/1072 half a year ago.

Working Example

I needed to change the code a bit to get a working starting point. See the code comments.

import matplotlib.pyplot as plt
import pandas as pd
import panel as pn
import param
import seaborn as sns


class MyCustomWidget(param.Parameterized):
    column = param.Selector()

    def __init__(self, data):
        super().__init__()

        columns = data.columns.values
        self.param.column.objects = columns
        self.column = columns[0]
        # Note: I need to set self.column to show a plot initially

    @param.depends("column")
    def plot(self):
        plt.close()
        # Note:
        # - I get exception if plt.close is below ax line. See https://github.com/holoviz/panel/issues/1482
        # - The plot does not change if I remove plot.close() fully.
        ax = sns.distplot(df[self.column])
        plot = pn.pane.Matplotlib(ax.figure, background="blue", width=300, height=300)
        return plot

df = pd.DataFrame(data={"x": [1, 2, 3, 4, 5, 6, 7], "y": [1, 2, 2, 4, 5, 9, 7]})
d = MyCustomWidget(df)
pn.Column(d.param, d.plot).servable()

Solution: View Parameter

This is the one I’ve gotten used to using.

import matplotlib.pyplot as plt
import pandas as pd
import panel as pn
import param
import seaborn as sns


class MyCustomWidget(param.Parameterized):
    column = param.Selector()
    view = param.Parameter()

    def __init__(self, data):
        super().__init__()

        columns = data.columns.values
        self.param.column.objects = columns
        self.column = columns[0]
        # Note: I need to set self.column to show a plot initially
        self.view = pn.Column(self.param.column, self.plot)

    @param.depends("column")
    def plot(self):
        plt.close()
        # Note:
        # - I get exception if plt.close is below ax line. See https://github.com/holoviz/panel/issues/1482
        # - The plot does not change if I remove plot.close() fully.
        ax = sns.distplot(df[self.column])
        plot = pn.pane.Matplotlib(ax.figure, background="blue", width=300, height=300)
        return plot

df = pd.DataFrame(data={"x": [1, 2, 3, 4, 5, 6, 7], "y": [1, 2, 2, 4, 5, 9, 7]})
MyCustomWidget(df).view.servable()

Pros:

  • Works technically well.
  • I have seen this is some examples in the official documentation (as far as I remember).
  • Conceptually is sort of a Controller View framework similar to the MVC (Model, View, Controller) framework.

Cons

  • Extra friction for the developer to document and explain to use the view method.
  • Extra friction for the user to understand and remember to use the the view method.
  • Not an “official” way to do this.
    • Other developers may choose another way to do this => Friction for developers and users.

Solution: Inheritance - Not working in Panel 0.9.7

This is how I initially thought it would work. But this example does not update when I select the y parameter and I’ve also experienced other problems in other examples previously. That is why I switched to using the view solution above.

See https://github.com/holoviz/panel/issues/1060 for more info on why this is not currently working.

I think the fundamental problem is that the inheritance use case has not been identified and therefore the implementation of Panel components currently (Panel 0.9.7) does not support this.

import matplotlib.pyplot as plt
import pandas as pd
import panel as pn
import param
import seaborn as sns


class MyCustomWidget(pn.Column):
    column = param.Selector()

    def __init__(self, data, **params):
        self._rename["column"] = None
        # Note:
        # I need to add _rename of `column` in order to not get the exception
        # AttributeError: unexpected attribute 'column' to Column, possible attributes are align, aspect_ratio, background, children, css_classes, disabled, height, height_policy, js_event_callbacks, js_property_callbacks, margin, max_height, max_width, min_height, min_width, name, rows, sizing_mode, spacing, subscribed_events, tags, visible, width or width_policy
        # The reason is that the code of Column tries to map every parameter on the class to a property on the underlying bokeh model.
        # and `column` is not a property on the underlying bokeh model.

        super().__init__(**params)

        columns = data.columns.values
        self.param.column.objects = columns
        self.column = columns[0]
        # Note: I need to set self.column to show a plot initially
        self[:] = [self.param.column, self.plot]

    @param.depends("column")
    def plot(self):
        print("update")
        plt.close()
        # Note:
        # - I get exception if plt.close is below ax line. See https://github.com/holoviz/panel/issues/1482
        # - The plot does not change if I remove plot.close() fully.
        ax = sns.distplot(df[self.column])
        plot = pn.pane.Matplotlib(ax.figure, background="blue", width=300, height=300)
        return plot


df = pd.DataFrame(data={"x": [1, 2, 3, 4, 5, 6, 7], "y": [1, 2, 2, 4, 5, 9, 7]})
MyCustomWidget(df).servable()

Pros:

  • Simple, natural way of constructing and using an instance of the component

Cons

  • Currently (Panel 0.9.7) not working.
  • There is a little bit of hazzle explaining why you need to add new parameters to the _rename dictionary.
  • A user could in fact change the content of the column.

Solution - Inheritance - Works in Panel 0.9.7

This works and can be instantiated and used as simple as MyCustomWidget(df). :+1:

custom-component

import matplotlib.pyplot as plt
import pandas as pd
import panel as pn
import param
import seaborn as sns


class MyCustomWidget(pn.Column):
    column = param.Selector()

    def __init__(self, data, **params):
        self._rename["column"] = None
        # Note:
        # I need to add _rename of `column` in order to not get the exception
        # AttributeError: unexpected attribute 'column' to Column, possible attributes are align, aspect_ratio, background, children, css_classes, disabled, height, height_policy, js_event_callbacks, js_property_callbacks, margin, max_height, max_width, min_height, min_width, name, rows, sizing_mode, spacing, subscribed_events, tags, visible, width or width_policy
        # The reason is that the code of Column tries to map every parameter on the class to a property on the underlying bokeh model.
        # and `column` is not a property on the underlying bokeh model.

        super().__init__(**params)

        self._plot_pane = pn.pane.Matplotlib(background="blue", width=300, height=300)
        self[:] = [self.param.column, self._plot_pane]

        self.param.watch(self._update_plot_pane, "column")
        # Note: Setting @param.depends("column", watch=True) on _update_plot_pane does not work.

        columns = data.columns.values
        self.param.column.objects = columns
        self.column = columns[0]
        # Note: I need to set self.column to show a plot initially

    def _update_plot_pane(self, _):
        plt.close()
        # Note:
        # - I get exception if plt.close is below ax line. See https://github.com/holoviz/panel/issues/1482
        # - The plot does not change if I remove plot.close() fully.

        ax = sns.distplot(df[self.column])
        self._plot_pane.object = ax.figure

df = pd.DataFrame(data={"x": [1, 2, 3, 4, 5, 6, 7], "y": [1, 2, 2, 4, 5, 9, 7]})
MyCustomWidget(df).servable()

Pros

  • Simple, natural way of constructing and using an instance of the component

Cons

  • The implementation is not a simple and flexible as it should be.
    • I need to _rename my custom parameters.
    • @param.depends("column", watch=True) is not working.
      • I need to use self.param.watch instead.
    • I cannot use objects like strings or plots in self[:].
      • I need to use layouts, panes or widgets in self[:]
    • The order of watch, plot_pane, self.column= matters.
      • (self.column= should be below the two others).
      • Could lead to confusion and questions from new Panel developers.
  • A user could in fact change the content of the column after construction.

For me this is the way to do it. And the cons above should be removed or minimized.

WHAT DO YOU THINK?

1 Like