Multi page app documentation

Hi,

I am pretty new to Panel, and while so far I love it, I am having trouble finding documentation or examples about building multi-page apps. My goal would be to have a sidebar with a list of pages, each containing some interactive plots.

With Dash I used to do that with Dash-bootstrap-components.

I suspect that Panel’s templates could be the solution but none of the examples shows a multi-page app.

The closest thing is this one here but with a card visualization rather than a sidebar with apps.

Any document I can read something about this?

Thanks

1 Like

Hi @micdonato ! I’m sure there are multiple ways of achieving this so I hope more people will chime in to bring their own experience.

I could suggest something like the following, where I use one of the templates provided by Panel to add buttons in the sidebar, clicking on a button updates the content of the main area. Well, too be more accurate, it updates the content of a Column layout that is the only component added here in the main area, since the main area should not be updated directly (this is mentioned multiple times in the documentation).

import panel as pn
import holobiews as hv
import nunpy as np

pn.extension()

vanilla = pn.template.VanillaTemplate(title='Vanilla Template')

xs = np.linspace(0,np.pi)
freq = pn.widgets.FloatSlider(name="Frequency", start=0, end=10, value=2)
phase = pn.widgets.FloatSlider(name="Phase", start=0, end=np.pi)

@pn.depends(freq=freq, phase=phase)
def sine(freq, phase):
    return hv.Curve((xs, np.sin(xs*freq+phase))).opts(
        responsive=True, min_height=400)

@pn.depends(freq=freq, phase=phase)
def cosine(freq, phase):
    return hv.Curve((xs, np.cos(xs*freq+phase))).opts(
        responsive=True, min_height=400)

page = pn.Column(sizing_mode='stretch_width')

content1 = [
    pn.Row(freq, phase),
    hv.DynamicMap(sine),
]
content2 = [
    pn.Row(freq, phase),
    hv.DynamicMap(cosine),
]

link1 = pn.widgets.Button(name='Sine')
link2 = pn.widgets.Button(name='Cosine')

vanilla.sidebar.append(link1)
vanilla.sidebar.append(link2)

vanilla.main.append(page)

def load_content1(event):
    vanilla.main[0].objects = content1


def load_content2(event):
    vanilla.main[0].objects = content2
    
link1.on_click(load_content1)
link2.on_click(load_content2)

vanilla.show()

So this ends up not being a multipage app per say, it’s just a rather dynamic single-page app.

A simpler solution, without sidebar though, would be to simply use Tabs.

1 Like

Hi @micdonato

Welcome to the community. The answer to your question depends on your use case and requirements.

Option 1 (preferred): panel serve multiple files

The simplest is to serve a multi page app using multiple files.

panel serve page1.py page2.ipynb page3.py ...

Add the --autoreload flag while developing. This will also give you the index page out of the box.

For example for panel-chemistry I just panel serve examples/reference/*.ipynb ( See Link). You can explore the multi-page application on binder here.

Option 2: pn.serve multiple functions or panels

For some use case you would need to use pn.serve instead and serve the application using python app.py. For example if you want to use other urls than the file names.

Run the script via python app.py

app.py

import panel as pn

def page1():
    return "page 1"

def page2():
    return pn.Column(
        "# Page 2", "Welcome to the second page"
    )
ROUTES = {
    "1": page1, "2": page2
}
pn.serve(ROUTES, port=5006)

Please note that if you don’t pn.serve functions but instances, then they will be shared across users/ sessions. The latter can be confusing. But it also makes it really simple interact in real time with the same app instance for example for demo/ testing purposes. Another use case for instances is a live updating dashboard that should look the same to all users.

Option 3 Use a Widget to navigate between pages in a Single Page Application.

panel serve or pn.serve one page and use a widget and/ or url arguments to navigate between “pages”.

import panel as pn

pn.extension(sizing_mode="stretch_width")

pages = {
    "Page 1": pn.Column("# Page 1", "...bla bla bla"),
    "Page 2": pn.Column("# Page 2", "...more bla"),
}


def show(page):
    return pages[page]


starting_page = pn.state.session_args.get("page", [b"Page 1"])[0].decode()
page = pn.widgets.RadioButtonGroup(
    value=starting_page,
    options=list(pages.keys()),
    name="Page",
    sizing_mode="fixed",
    button_type="success",
)
ishow = pn.bind(show, page=page)
pn.state.location.sync(page, {"value": "page"})

ACCENT_COLOR = "#0072B5"
DEFAULT_PARAMS = {
    "site": "Panel Multi Page App",
    "accent_base_color": ACCENT_COLOR,
    "header_background": ACCENT_COLOR,
}
pn.template.FastListTemplate(
    title="As Single Page App",
    sidebar=[page],
    main=[ishow],
    **DEFAULT_PARAMS,
).servable()

Please note the downside of this approach is that when Panel/ Bokeh replaces one page with another it can be slow if your layout is complex. This should hopefully be fixed when bokeh 3.0 is released.

If you want to have multiple pages in one “single page app” consider using Tabs with argument dynamic=True or a custom template. That will make you app more snappy.

Share State Across Pages

If you need to share state across your pages you can use pn.state.cache for that. It’s just normal python dictionary. You can share Parameterized Classes, DataFrames, Widgets etc if you need to.

Lets take an example show casing some of the possibilities

You can run the below via python name_of_script.py. Alternatively you can refactor out into seperate files and panel serve them.

import panel as pn
import param
import datetime
from threading import Thread
import time

pn.extension(sizing_mode="stretch_width")
class StreamClass(param.Parameterized):
    value = param.Integer()
class MessageQueue(param.Parameterized):
    value = param.List()

    def append(self, asof, user, message):
        if message:
            self.value = [*self.value, (asof, user, message)]

ACCENT_COLOR = "#0072B5"
DEFAULT_PARAMS = {
    "site": "Panel Multi Page App",
    "accent_base_color": ACCENT_COLOR,
    "header_background": ACCENT_COLOR,
}

def fastlisttemplate(title, *objects):
    """Returns a Panel-AI version of the FastListTemplate

    Returns:
        [FastListTemplate]: A FastListTemplate
    """
    return pn.template.FastListTemplate(**DEFAULT_PARAMS, title=title, main=[pn.Column(*objects)])


def get_shared_state():
    if not "stream" in pn.state.cache:
        state=pn.state.cache["stream"]=StreamClass()
        pn.state.cache["messages"]=MessageQueue()

        def update_state():
            while True:
                if state.value==100:
                    state.value=0
                else:
                    state.value+=1
                time.sleep(1)

        Thread(target=update_state).start()

    return pn.state.cache["stream"], pn.state.cache["messages"]

def show_messages(messages):
    result = ""
    for message in messages:
        result = f"- {message[0]} | {message[1]}: {message[2]}\n" + result
    if not result:
        result = "No Messages yet!"
    return result

def page1():
    _, messages = get_shared_state()

    user_input = pn.widgets.TextInput(value="Guest", name="User")
    message_input = pn.widgets.TextInput(value="Hello", name="Message")
    add_message_button = pn.widgets.Button(name="Add")

    def add_message(event):
        messages.append(datetime.datetime.utcnow(), user_input.value, message_input.value)
    add_message_button.on_click(add_message)

    return fastlisttemplate("Add Message", user_input, message_input, add_message_button)

def page2():
    _, messages = get_shared_state()

    ishow_messages = pn.bind(show_messages, messages=messages.param.value)
    return fastlisttemplate("Show Messages",pn.panel(ishow_messages, height=600),)

def page3():
    stream, _ = get_shared_state()

    return fastlisttemplate("Show Streaming Value",stream.param.value,)

ROUTES = {
    "1": page1, "2": page2, "3": page3
}
pn.serve(ROUTES, port=5006, autoreload=True)
5 Likes