Panel App deployment as standalone interactive HTML file

Hi,

in a previous issue I have gotten my Panel App running thanks to the help of the community. Now I am facing the last step, which entails deploying my app as a standalone HTML file that keeps the interactivity given by the widgets. Here is the code to reproduce it in the Jupyter environment:

from logging import setLogRecordFactory
import param
import numpy as np
import pandas as pd
import panel as pn
import holoviews as hv
import bokeh
import hvplot.pandas
from holoviews.plotting.util import process_cmap


hv.extension("bokeh")
pn.extension()
pn.config.sizing_mode = "stretch_width"

EMPTY_PLOT = hv.Curve({})
COLOR_MAPS = hv.plotting.util.list_cmaps()
STYLE = """
body {
    margin: 0px;
    min_height: 100vh;
}
.bk.app-body {
    background: #f2f2f2;
    color: #000000;
    font-family: roboto, sans-serif, Verdana;
}
.bk.app-bar {
    background: #212121;
    border-color: white;
    box-shadow: 5px 5px 20px #9E9E9E;
    color: #ffffff;
    z-index: 50;
}
.bk.app-container {
    background: #ffffff;
    border-radius: 5px;
    box-shadow: 2px 2px 2px lightgrey;
    color: #000000;
}

.bk.app-settings {
    background: #e0e0e0;
    color: #000000;
}

"""

pn.config.raw_css.append(STYLE)

try:
    data_A = pd.read_csv("data_A.csv", index_col=0)
except Exception as e:
    data_A = pd.read_csv(
        "https://discourse.holoviz.org/uploads/short-url/ceLgCS43UtYgICERGGnBpTxG4UX.csv",
        index_col=0,
    )
    data_A.to_csv("data_A.csv")

try:
    data_B = pd.read_csv("data_B.csv", index_col=0)
except Exception as e:
    data_B = pd.read_csv(
        "https://discourse.holoviz.org/uploads/short-url/mLsBXvpSQTex5rU6RZpzsaxOe5b.csv",
        index_col=0,
    )
    data_B.to_csv("data_B.csv")

Variable = pn.widgets.RadioBoxGroup(
    name="Variable",
    options=["Cut Distance", "Removed Volume", "Av. uncut chip thickness"],
    inline=True,
    align="center",
)

# Insert plot
class PempDashoardApp(param.Parameterized):
    tool = param.ObjectSelector(label="Tool", default="S1_1", objects=["S1_1", "S2_1"])
    variable = param.ObjectSelector(
        label="Variable",
        default="Cut Distance",
        objects=["Cut Distance", "Removed Volume", "Av. uncut chip thickness"],
    )
    color_map = param.ObjectSelector(default="winter", objects=COLOR_MAPS)

    insert_plot_pane = param.ClassSelector(class_=pn.pane.HoloViews)
    edge_plot_pane = param.ClassSelector(class_=pn.pane.HoloViews)
    history_plot_pane = param.ClassSelector(class_=pn.pane.HoloViews)

    view = param.ClassSelector(class_=pn.Column)

    def __init__(self, **params):
        params["insert_plot_pane"] = pn.pane.HoloViews(EMPTY_PLOT, sizing_mode="stretch_both", margin=10)
        params["edge_plot_pane"] = pn.pane.HoloViews(EMPTY_PLOT, sizing_mode="stretch_both", margin=10)
        params["history_plot_pane"] = pn.pane.HoloViews(EMPTY_PLOT, sizing_mode="stretch_both", margin=10)
        params["view"] = pn.Column(css_classes=["app-body"], sizing_mode="stretch_both", margin=0)

        super().__init__(**params)

        self._init_view()
        self._update_insert_plot()
        self._update_edge_plot()
        self._update_history_plot()

    def _init_view(self):
        appbar = pn.Row(
            pn.pane.Markdown("# Classic Dashboard in Panel ", margin=(10, 5, 10, 25)),
            pn.Spacer(height=0),
            pn.pane.PNG(
                "https://panel.holoviz.org/_static/logo_horizontal.png",
                width=200,
                sizing_mode="fixed",
                align="center",
                margin=(10, 50, 10, 5),
            ),
            css_classes=["app-bar"],
        )
        settings_bar = pn.Row(
            pn.Param(
                self,
                parameters=["tool", "variable"],
                widgets={
                    "tool": {"align": "center", "width": 75, "sizing_mode": "fixed"},
                    "variable": {"type": pn.widgets.RadioBoxGroup, "inline": True, "align": "end",},
                },
                default_layout=pn.Row,
                show_name=False,
                align="center",
            ),
            pn.Spacer(height=0),
            pn.Param(
                self,
                parameters=["color_map"],
                width=200,
                align="center",
                sizing_mode="fixed",
                show_name=False,
                margin=(10, 25, 10, 5),
            ),
            sizing_mode="stretch_width",
            css_classes=["app-container"],
            margin=(50, 25, 25, 25),
        )

        self.view[:] = [
            appbar,
            settings_bar,
            pn.Row(
                pn.Column(self.insert_plot_pane, css_classes=["app-container"], margin=25),
                pn.Column(self.edge_plot_pane, css_classes=["app-container"], margin=25),
            ),
            pn.Row(self.history_plot_pane, css_classes=["app-container"], margin=25),
            pn.layout.VSpacer(),
            pn.layout.VSpacer(),
        ]

    @pn.depends("tool", "variable", "color_map", watch=True)
    def _update_insert_plot(self):
        plot_data = data_A.loc[self.tool]
        data = [(plot_data["Xo"], plot_data["Yo"], plot_data[self.variable])]
        self.insert_plot_pane.object = hv.Path(data, vdims=self.variable).opts(
            cmap=self.color_map, color=self.variable, line_width=4, colorbar=True
        )

    @pn.depends("tool", "variable", "color_map", watch=True)
    def _update_edge_plot(self):
        plot_data = data_A.loc[self.tool]
        self.edge_plot_pane.object = plot_data.hvplot(
            x="Number", y=self.variable, kind="area", alpha=0.6, color=self.get_color
        )

    @pn.depends("tool", "color_map", watch=True)
    def _update_history_plot(self):
        plot_data = data_B.loc[self.tool]
        self.history_plot_pane.object = plot_data.hvplot(
            x="Cut Distance", y="Feed", kind="line", line_width=4
        ).opts(color=self.get_color)

    @property
    def get_color(self):
        return process_cmap(self.color_map, 1)[0]


def view():
    return PempDashoardApp().view

view()

In order to obtain a html file I use the save function as follows:

view().save('test', embed=True, resources=INLINE)

I do obtain a html file, but the interactivity has been apparently lost. Does anyone know hot to solve this issue?

Thanks in advance!

I’m not 100% sure but I think this may just be a bit too ambitious… From the docs on Deploy and Export:

As you might imagine if there are multiple widgets there can quickly be a combinatorial explosion of states so by default the output is limited to about 1000 states. For larger apps the states can also be exported to json files, e.g. if you want to serve the app on a website specify the save_path to declare where it will be stored and the load_path to declare where the JS code running on the website will look for the files.

You don’t really have that many combinations of input widgets, but there is a lot of data so I wonder if that is causing the problems. I would have thought there’d be a warning if it was cutting you off though.

Anyway, I’m not sure that this is the problem but thought it was worth flagging up that there are definitely limitations to save/embed.

Anyone else know any better…?

3 Likes

I think the issue is that your app requires a python kernel for the interactive components and therefore cannot run as a standalone html.

Your code in the functions decorated with pn.depends do some python work such as select the data to plot. If you could rewrite your app to use javascripts callbacks only it might be possible to make a standalone html with interactivity.

4 Likes

Thank you for the reply. It seems pretty reasonable to think that’s the cause for the behaviour I see. I guess I will have to find a more suitable way to deploy my app.