Beginner layout design patterns?

Hi all,

Been diving into panel the past week and really loving it, especially the new .rx paradigm that seems really powerful and intuitive (but I need to dive into it more!)

However, while working through the tutorials and building a sample app of my own, I definitely felt more of a learning curve, compared to coming from solara and python shiny. What puzzled me is that panel is very well organized and logical, but getting things to work just took a bit more work – there were more details to get right.

After reflecting, I realized that what I’m missing for fast exploratory prototyping is a more linear and lightweight design/layouting pattern. E.g. components rendered where they’re specified (I believe this can be handled with .servable(target=...)), and context managers (e.g. with Row... with Column... with Sidebar...) to layout more hierarchical layouts.

I understand the way that panel helps us to organize code, but sometimes I need something simple and the above design patterns are intuitive python (e.g. how one almost expects to do things, without documentation), even if not properly maintainable for large apps.

Am I missing something basic to achieve this? If so, please ignore the details below.

===== Examples Below =====

For example, in Solara, one can do the following that mixes data processing with UI displays in a single function. It might not be the “best” design pattern, but for rapid prototyping and experimentation the mental overhead is very light: add a decorator (that doesn’t need arguments!), call reactive_variable.value (similar to panel’s .value/.rx), and use components almost as stand-ins for “print” statements.

x = solara.reactive(1) #very similar to panel's pn.rx() feature
y = solara.Select(...)
z = solara.reactive(42)

@solara.component
def View():
    x_encoded = format_input(x.value, y.value, z.value)
    res = {}
    for output,model in models.items():
        res[output] = model.predict(x_encoded)[0]
        solara.Markdown(f"{output:}:\t\t{res[output]}")
View()

In panel, one needs a subtle shift in design patterns to do something like:

x = pn.widgets.Select(...)
y = pn.widgets.Select(...)
z = pn.widgets.Select...)

def forward_inference(x,y,z):
    x_encoded = format_input(x,y,z)
    res = {}
    for output,model in models.items():
        res[output] = model.predict(x_encoded)[0]
        if isinstance(res[output],float):
            res[output] = np.round(res[output],1)
    return res

pn.panel(pn.rx(forward_inference)(pg_pn,metal_pn,feed_pn,medium_pn)).servable()

(or one could use @pn.depends, or pn.bind, or probably several other patterns)

New design details to learn and keep track of:

  • explicitly remembering to bind function to variables
  • abstracting things into a forward inference function that one subsequently reactifies + explicitly declare the reactive variables to watch for (probably best to do this eventually, but while doing initial developing it almost forced me to create an abstraction level while I was still testing/figuring out functionality)
  • creating a pane, and remembering to make it servable
  • separating display from code (again, good reasons to do this for maintainability, but slows things down for exploratory deployments).

Not terribly hard, but the nuances add up and I did find myself stumbling my first time through trying to create a multipage dashboard. (I was also mostly using pn.bind; I believe pn.rx could’ve simplified things).

For more complex layouts, Solara, like Shiny Express and Streamlit, uses context managers, like with solara.Column... that one can then nest. Again, for more complex apps and maintainability one definitely would rather separate out the GUI and the processing code like Panel and Shiny Core (and even Dash), but for getting started, sometimes it’s just easier to have everything in one place, as in via the context manager design pattern.

My personal opinion is to stick with one API (e.g. pn.bind) and get really good with using it, or else trying to use all the APIs can get confusing.

This is how I usually approach building simple apps in Panel.

  1. Draw some diagram
  2. Create the widgets
  3. Lay them out
  4. Add interactivity
  5. Optimize UX
    Transform a Python script into an interactive, web app, and make it performant | by Andrew Huang | Stackademic

This draft of best practices might also help

1 Like

Thank you! The linked you shared were very helpful.
Been using panel a bit more now, and pn.bind is becoming more intuitive now.

2 Likes

Hi @kshen-noble

In the concrete example I would be using @pn.depends

import panel as pn

pn.extension(design="material")

OPTIONS=['a','b','c']

x = pn.widgets.Select(options=['x1', 'x2', 'x3'])
y = pn.widgets.Select(options=['y1', 'y2', 'y3'])
z = pn.widgets.Select(options=['z1', 'z2', 'z3'])

@pn.depends(x,y,z)
def forward_inference(x,y,z):
    return f"{x}, {y}, {z}"

pn.Column(x,y,z,forward_inference).servable()

My favorite approach is a class based, Parameterized approach.

import panel as pn
import param

pn.extension(design="material")

OPTIONS=['a','b','c']

class State(param.Parameterized):
    x = param.Selector(objects=['x1', 'x2', 'x3'])
    y = param.Selector(objects=['y1', 'y2', 'y3'])
    z = param.Selector(objects=['z1', 'z2', 'z3'])

    def forward_inference(self):
        return f"{self.x}, {self.y}, {self.z}"

state = State()

 

pn.Column(state.param.x, state.param.y, state.param.z,state.forward_inference).servable()

Thanks!