Skip to content

Build serverless web application with Holoviz Panel

Panel is a Python library for building web applications, mainly serves as a bridge between Bokeh and HoloViews. (If you don't use HoloViews directly or indirectly, you may want to skip this topic and search for some alternative libraries.)
Panel runs on both CPython and Pyodide. For installation, please refer to the tutorial. Beyond that, in Mahoraga we provide proxy for Panel/Bokeh frontend assets, eliminating direct requests to upstream servers completely. The following example is for Pyodide, but can be adapted to CPython as well.

Note

Mahoraga serves on http://127.0.0.1:3450 by default. Replace it with the actual URL exposed to your clients.

Prerequisites

  • A running Mahoraga server which we have set up in the previous tutorial.
  • CORS must be enabled.

Simple slider panel

The following example is just a combination of the Mahoraga, Panel and Pyodide tutorials. The key point is the script panel_support.py which replaces all the hard-coded URLs in Panel with a runtime configurable prefix, i.e. the Mahoraga URL.

<!doctype html>
<html>
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script type="text/javascript" src="http://127.0.0.1:3450/pyodide/v0.28.2/full/pyodide.js"></script>
    <script type="text/javascript" src="http://127.0.0.1:3450/npm/@bokeh/bokehjs@3.6.3/build/js/bokeh.min.js"></script>
    <script type="text/javascript" src="http://127.0.0.1:3450/npm/@bokeh/bokehjs@3.6.3/build/js/bokeh-widgets.min.js"></script>
    <script type="text/javascript" src="http://127.0.0.1:3450/npm/@bokeh/bokehjs@3.6.3/build/js/bokeh-tables.min.js"></script>
    <script type="text/javascript" src="http://127.0.0.1:3450/npm/@holoviz/panel@1.7.5/dist/panel.min.js"></script>
  </head>
  <body>
    <div id="simple_app"></div>
    <script type="text/javascript">
      async function main() {
        let pyodide = await loadPyodide();
        await pyodide.loadPackage("micropip");
        pyodide.runPython(`
            import micropip
            class _Transaction(micropip.transaction.Transaction):
                def __post_init__(self):
                    super().__post_init__()
                    self.search_pyodide_lock_first = True
            micropip.package_manager.Transaction = _Transaction
        `);
        const micropip = pyodide.pyimport("micropip");
        micropip.set_index_urls("http://127.0.0.1:3450/pypi/simple/{package_name}/?micropip=1");
        await micropip.install(["panel ==1.7.5"]);
        await pyodide.runPythonAsync(`
            from pyodide.http import pyfetch
            response = await pyfetch("http://127.0.0.1:3450/static/panel_support.py")
            with open("panel_support.py", "wb") as f:
                f.write(await response.bytes())
        `);
        pyodide.runPython(`
            import panel_support
            panel_support.monkey_patch("http://127.0.0.1:3450/")
            import panel as pn
            pn.extension(sizing_mode="stretch_width")
            slider = pn.widgets.FloatSlider(start=0, end=10, name="Amplitude")
            def callback(new):
                return f"Amplitude is: {new}"
            pn.Row(slider, pn.bind(callback, slider)).servable(target="simple_app")
        `);
      }
      main();
    </script>
  </body>
</html>
<!doctype html>
<html>
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script type="text/javascript" src="http://127.0.0.1:3450/npm/@bokeh/bokehjs@3.6.3/build/js/bokeh.min.js"></script>
    <script type="text/javascript" src="http://127.0.0.1:3450/npm/@bokeh/bokehjs@3.6.3/build/js/bokeh-widgets.min.js"></script>
    <script type="text/javascript" src="http://127.0.0.1:3450/npm/@bokeh/bokehjs@3.6.3/build/js/bokeh-tables.min.js"></script>
    <script type="text/javascript" src="http://127.0.0.1:3450/npm/@holoviz/panel@1.7.5/dist/panel.min.js"></script>
    <script type="module" src="http://127.0.0.1:3450/npm/@pyscript/core@0/dist/core.js"></script>
    <link rel="stylesheet" href="http://127.0.0.1:3450/npm/@pyscript/core@0/dist/core.css">
  </head>
  <body>
    <div id="simple_app"></div>
    <script type="py" config='{
      "interpreter": "http://127.0.0.1:3450/pyodide/v0.28.2/full/pyodide.mjs",
      "packages": ["bokeh"]
    }'>
        import micropip
        class _Transaction(micropip.transaction.Transaction):
            def __post_init__(self):
                super().__post_init__()
                self.search_pyodide_lock_first = True
        micropip.package_manager.Transaction = _Transaction
        micropip.set_index_urls("http://127.0.0.1:3450/pypi/simple/{package_name}/?micropip=1")
        await micropip.install(["panel ==1.7.5"])

        from pyodide.http import pyfetch
        response = await pyfetch("http://127.0.0.1:3450/static/panel_support.py")
        with open("panel_support.py", "wb") as f:
            f.write(await response.bytes())

        import panel_support
        panel_support.monkey_patch("http://127.0.0.1:3450/")
        import panel as pn
        pn.extension(sizing_mode="stretch_width")
        slider = pn.widgets.FloatSlider(start=0, end=10, name="Amplitude")
        def callback(new):
            return f"Amplitude is: {new}"
        pn.Row(slider, pn.bind(callback, slider)).servable(target="simple_app")
    </script>
  </body>
</html>