Allow user to download parts of the app as static HTML

Continuing the discussion from Download data underlying holoviews/bokeh plot:

I am trying to allow the user to be able to save parts of the UI as a static HTML with the embedded data required for those plots.

The context being a massive Xarray dataset that uses box select to calculate stats on the subset and I want to allow my colleagues to download the resulting Holoviews plots as static HTML (preferably as the template) directly from the browser (I do not want to save the HTML in the App root directory but rather as a download in the browser client side).

I’ve used approaches like that of Mr. @Marc in other apps and have tried Mr. @philippjfr 's solution here but I get a blank HTML and so I wonder if what I am trying to accomplish is doable?

import panel as pn
import holoviews as hv
import numpy as np
from bokeh.resources import INLINE

ACCENT_COLOR = "#308752"

pn.extension(
    sizing_mode="stretch_width",
    notifications=True
)

class DownloadClientSide(pn.viewable.Viewer):
    def _load_callback(self, event):
        points = hv.Points(np.random.randn(1000, 2))
        selection = hv.streams.Selection1D(source=points)
        def selected_info(index):
            arr = points.array()[index]
            if index:
                label = 'Mean x, y: %.3f, %.3f' % tuple(arr.mean(axis=0))
            else:
                label = 'No selection'
            return points.clone(arr, label=label).opts(color='red')

        selected_points = hv.DynamicMap(selected_info, streams=[selection])
        layout = points.opts(tools=['box_select', 'lasso_select']) + selected_points
        self.figs_col.append(pn.pane.HoloViews(layout))
        pn.state.notifications.success('_load_callback')

    def _download_callback(self):
        # pn.save(self.figs_col, 'test.html', resources=INLINE)
        # self.figs_col.save('test.html')
        pn.state.notifications.success('_download_callback')
        sio = io.StringIO()
        self.figs_col.save(sio)
        sio.seek(0)
        return sio
    
    def __init__(self):
        self.figs_col = pn.Column()
        self.load_button = pn.widgets.Button(name="Load")
        self.load_button.on_click(self._load_callback)
        self.download_button = pn.widgets.FileDownload(filename='layout.html', callback=self._download_callback)
        # self.download_button = pn.widgets.Button(name="Download")
        # self.download_button.on_click(self._download_callback)
        super().__init__()
        
    def view(self):
        return self.download_button

app = DownloadClientSide()

template = pn.template.FastListTemplate(
    accent_base_color=ACCENT_COLOR,
    header_background=ACCENT_COLOR,
    title="Download Client Side",
    main=[app.load_button, app.download_button, app.figs_col],
    sidebar_width=200,
    theme="dark",
).servable()
1 Like

If you add the line import io you will actually get a download.

But the downloaded .html file will not work because it lacks some Fast related javascript files. This is a bug and should be filed on Github.

1 Like

I already had io imported Mr. @Marc and sure I will report it. Do you think I can create a Material Template and save the whole template in the meantime?

Here is the GitHub Issue.

@Marc changing to MaterialTemplate works and produces the following document
layout.html (178.9 KB)

however as you can see it no longer has interactivity (understandibly so). Is there a way to keep it?

import panel as pn
import holoviews as hv
import numpy as np
from bokeh.resources import INLINE

ACCENT_COLOR = "#308752"

pn.extension(
    sizing_mode="stretch_width",
    notifications=True
)

class DownloadClientSide(pn.viewable.Viewer):
    def _load_callback(self, event):
        points = hv.Points(np.random.randn(1000, 2))
        selection = hv.streams.Selection1D(source=points)
        def selected_info(index):
            arr = points.array()[index]
            if index:
                label = 'Mean x, y: %.3f, %.3f' % tuple(arr.mean(axis=0))
            else:
                label = 'No selection'
            return points.clone(arr, label=label).opts(color='red')

        selected_points = hv.DynamicMap(selected_info, streams=[selection])
        layout = points.opts(tools=['box_select', 'lasso_select']) + selected_points
        self.figs_col.append(pn.pane.HoloViews(layout))
        pn.state.notifications.success('_load_callback')

    def _download_callback(self):
        # pn.save(self.figs_col, 'test.html', resources=INLINE)
        # self.figs_col.save('test.html')
        pn.state.notifications.success('_download_callback')
        sio = io.StringIO()
        pn.template.MaterialTemplate(
            accent_base_color=ACCENT_COLOR,
            header_background=ACCENT_COLOR,
            title="Export Test",
            main=[self.figs_col],
            sidebar_width=200,
            theme="dark",
        ).save(sio)
        # self.figs_col[0].save(sio)
        sio.seek(0)
        return sio
    
    def __init__(self):
        self.figs_col = pn.Column()
        self.load_button = pn.widgets.Button(name="Load")
        self.load_button.on_click(self._load_callback)
        self.download_button = pn.widgets.FileDownload(filename='layout.html', callback=self._download_callback)
        # self.download_button = pn.widgets.Button(name="Download")
        # self.download_button.on_click(self._download_callback)
        super().__init__()
        
    def view(self):
        return self.download_button

app = DownloadClientSide()

template = pn.template.FastListTemplate(
    accent_base_color=ACCENT_COLOR,
    header_background=ACCENT_COLOR,
    title="Download Client Side",
    main=[app.load_button, app.download_button, app.figs_col],
    sidebar_width=200,
    theme="dark",
).servable()