Personal opinions about best practices for Panel + HoloViews

Since I just finished creating one of my most polished apps yet (Year vs Climatology - a Hugging Face Space by ahuang11) and I think it has a lot of examples of best practices (mostly personal opinions based on my experience using HoloViz), I randomly decided to do a terribly unpolished brain dump! For a more coherent version by ChatGPT, scroll down below :slight_smile:

I like to…

  1. start small, and iteratively build up; panel serve app.py --autoreload is pretty handy
  2. try to layout everything first, or at least the core, and make sure it looks right
  3. if it looks right then add interactivity with pn.bind or pn.depends

repeat steps 1-3 continuously when adding something new, e.g. create placeholder widgets/panes, ensure it looks right, and add interactions

I think that’s about it… if these don’t make sense, let me know and I’ll try to clarify!

14 Likes

A more coherent version by ChatGPT
“”"
After completing the development of what I consider one of my most refined applications yet (Year vs Climatology - a Hugging Face Space by ahuang11), I realized it encompasses numerous examples of best practices, mostly drawn from my experience using HoloViz. In a spontaneous decision, I decided to compile a somewhat unpolished brain dump of these practices:

  • Instead of inheriting from param.Parameterized, opting for inheritance from pn.viewable.Viewer allows direct invocation of the class, resembling a native Panel object. This eliminates the need for calling a method like ClimateApp().view().servable().
  • To ensure sliders only trigger callbacks upon mouse-up, set throttled=True.
  • Start by instantiating the initial layout with placeholder pn.Columns in __init__, then populate it later in onload.
  • Enhance app responsiveness by employing onload to prevent webpage hang-ups during slow loading processes. Users can immediately observe a loading indicator upon visiting, signaling ongoing processing.
  • Manage loading states effectively by encapsulating widget activation within a try/finally block. This ensures widgets are always reactivated upon completion or in the event of errors.
  • Utilize pn.Param or from_param classmethod for converting class parameters to widgets, with the latter particularly useful for handling nested dictionaries.
  • Renaming widget names can be achieved using label when utilizing pn.Param or from_param.
  • Ensure proper param watching/binding by using self.param.NAME_OF_PARAM instead of self.NAME_OF_PARAM, noting the additional .param..
  • Optimize runtime and reduce flickers by updating object and value on pre-initialized placeholders instead of recreating the panel object every time pn.bind runs.
  • Preserve plot zoom ranges during updates by wrapping hv.DynamicMap with pn.bind. This prevents the plot from resetting zoom, although some bugs may necessitate workarounds for issues like title/ylabel updating.
  • Enhance app speed using @pn.cache, but remember that pn.cache activation depends on input kwargs.
  • Employ pn.depends with watch=True when a method doesn’t return anything.
  • Utilize a template’s modal (pop-up container) to optimize screen estate if unsure where else to place content.
  • Bind to HoloViews streams, noting similarities and differences between hv.DynamicMap streams and pn.bind.
  • Prevent text overlapping by setting opposites with text align and baseline.

My approach typically involves:

  1. Starting with small iterations, gradually building up functionality. The panel serve app.py --autoreload command proves handy in this regard.
  2. Focusing initially on layout, ensuring the core structure looks correct.
  3. Introducing interactivity with pn.bind or pn.depends once the layout is satisfactory.

I continuously iterate through these steps when implementing new features, beginning with creating placeholder widgets/panes, verifying visual correctness, and finally adding interactivity. If any of these concepts require clarification, feel free to ask!
“”"

3 Likes

Thanks for sharing.

My learnings are

3 Likes

Awesome! Wasn’t aware of defer_load.

Also, I think we should put these in the docs as Best Practices…

Some inspiration:
Dask Best Practices — Dask documentation
10 React Antipatterns to Avoid - Code This, Not That! (youtube.com)

3 Likes
  • Updating panel components in a function annotated with watch=True is an anti-pattern. Running some other side effect is fine.

What is the best practice pattern to update a panel component instead?

3 Likes

I was also confused about this; see Update docs to use the bound function as a reference · Issue #6271 · holoviz/panel · GitHub

1 Like

I agree with this approach, but I would clarify that it applies if your class contains Panel code (not just Param code). I.e., you should just use Parameterized while your class contains only Param items, but once you throw in anything to do with Panel (such as a view() method that returns a Panel layout, or it has explicit Panel widgets) then you should go ahead and make it a Viewer so that you aren’t duplicating the code that’s already in Viewer.

3 Likes

I was reading through the panel pipeline documentation recently: Create a Pipeline — Panel v1.3.8

And I noticed that it suggests a pattern of having output, view, and panel methods on parameterized classes that are intended to be used with panel. I was thinking, this pattern could be used as a design pattern beyond the use of pipelines.


class Stage1(param.Parameterized):

    a = param.Integer(default=2, bounds=(0, 10))
    b = param.Integer(default=3, bounds=(0, 10))

    @param.output(('c', param.Integer), ('d', param.Integer))
    def output(self):
        return self.a * self.b, self.a ** self.b

    @param.depends('a', 'b')
    def view(self):
        c, d = self.output()
        c_out = pn.pane.LaTeX('${a} * {b} = {c}$'.format(
            a=self.a, b=self.b, c=c), styles={'font-size': '2em'})
        d_out = pn.pane.LaTeX('${a}^{{{b}}} = {d}$'.format(
            a=self.a, b=self.b, d=d), styles={'font-size': '2em'})
        return pn.Column(
		    c_out, d_out,  margin=(40, 10), styles={'background': '#f0f0f0'}
		)

    def panel(self):
        return pn.Row(self.param, self.view,)

Does this make sense, and would this standard be complimentary to the concept of inheriting from pn.viewable.Viewer?

2 Likes

I’ve had trouble understanding the difference between view + panel, but I think your example helps me understand it much better (view is the main of the template and panel is the sidebar so to speak!)

I am wondering though, what output would be for more complicated apps?

1 Like

To just add this to eventual HoloViews best practice guide; sorting / dropping NAs for curves

@ahuang11, have you ever faced the problem with the geoviz/cartopy map + dynamic map not showing up in the modal pane?
All works fine in main and side panel but the modal panel stays empty when I add a pane with geoviews map and holoviews DynamicMap :man_shrugging:

I have not; are you able to reproduce with MRVE? Also maybe check browser for any errors

It works in Firefox, but not in Chrome nor Edge.

What is MRVE?

Minimal reproducible verifiable example (I guess just MRE)

1 Like

Oh, sure :slight_smile:

I ran the following code in python3.10, panel 1.3.8 and geoviews 1.11.1:

import panel as pn
from geoviews import tile_sources


class MinimalModalApp(pn.viewable.Viewer):
    def __init__(self):
        self.button = pn.widgets.Button(name="open modal")
        self.button.on_click(self.open_modal)

        self.modal = pn.Column(
            pn.pane.Markdown("### Worldcard:"),
            self.get_worldcard(),
            align="center")

        self.template = pn.template.FastListTemplate(
            main=[self.button],
            modal=[self.modal],
            title="MVPE - Worldmap not displayed in modal in Chromium based browsers (Chrome/Edge, Firefox works)"
        )

    def get_worldcard(self):
        return tile_sources.tile_sources["CartoEco"].opts(width=800, height=400)

    def open_modal(self, event):
        self.template.open_modal()

    def __panel__(self):
        return self.template


if __name__ == "__main__":
    app = MinimalModalApp()
    pn.serve(app, port=5006)

I took a quick look:

Something with xlim/ylim I think, but I couldn’t get it working yet

import panel as pn
from geoviews import tile_sources
import cartopy.crs as ccrs
import geoviews as gv


class MinimalModalApp(pn.viewable.Viewer):
    def __init__(self):
        self.button = pn.widgets.Button(name="open modal")
        self.button.on_click(self.open_modal)

        modal = pn.pane.HoloViews(self.get_worldcard())
        self.modal = pn.Column(
            pn.pane.Markdown("### Worldcard:"),
            modal,
            align="center")

        self.template = pn.template.FastListTemplate(
            main=[self.button],
            modal=[self.modal],
            title="MVPE - Worldmap not displayed in modal in Chromium based browsers (Chrome/Edge, Firefox works)"
        )

    def get_worldcard(self):
        return gv.Points((0, 0), crs=ccrs.GOOGLE_MERCATOR).opts(global_extent=True) * tile_sources.CartoDark()

    def open_modal(self, event):
        self.template.open_modal()

    def __panel__(self):
        return self.template


MinimalModalApp().servable()

It seems like it’s not just tile sources, but generic plots

import panel as pn
from geoviews import tile_sources
import cartopy.crs as ccrs
import geoviews as gv


class MinimalModalApp(pn.viewable.Viewer):
    def __init__(self):
        self.button = pn.widgets.Button(name="open modal")
        self.button.on_click(self.open_modal)

        self._map = gv.Points((0, 0)).opts(xlim=(-90, 90), ylim=(-90, 90), width=500, height=500)
        self.modal = pn.Column(
            pn.pane.Markdown("### Worldcard:"),
            pn.pane.HoloViews(self._map, width=800, height=400),
            align="center")

        self.template = pn.template.FastListTemplate(
            main=[self.button, self._map.clone()],
            modal=[self.modal],
            title="MVPE - Worldmap not displayed in modal in Chromium based browsers (Chrome/Edge, Firefox works)"
        )

    def open_modal(self, event):
        self.template.open_modal()

    def __panel__(self):
        return self.template


MinimalModalApp().servable()

Use persist() or load() if data fits in memory to prevent re-triggering computations.

I don’t understand. Which object should be persist() or load() be applied on?

See this for context.