Python Module (Shinylive)

A serverless WebAssembly-powered Python Shiny app demonstrating the implementation of custom modules in Python using shiny.module.
Author

Brian S. Yandell

← Back to Demos Gallery

This section displays a Python geyser app using inst/build_module/5_python/app_hist.py, powered directly in the page via Shinylive (WebAssembly).

The live application is run completely serverless in your browser. Below the application, you can view the Python source files for both the main application (app_hist.py) and the custom module (geyser/hist.py) in separate tabs.

#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
#| standalone: true
#| viewerHeight: 800
#| components: [viewer]

## file: requirements.txt
pandas
numpy
matplotlib
scipy

## file: app.py
from shiny import App, render, ui
from geyser.hist import hist_server, hist_input, hist_output, hist_ui

app_ui = ui.page_fluid(
    hist_input("hist"),
    hist_output("hist"),
    hist_ui("hist")
)

def app_server(input, output, session):
    hist_server("hist")

app = App(app_ui, app_server)

## file: geyser/__init__.py
# Package marker

## file: geyser/io.py
# Mock io module for WebAssembly environment
def app_run(app, host="127.0.0.1", port=None):
    pass

## file: geyser/hist.py
from shiny import App, module, reactive, render, ui
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy.stats import gaussian_kde

@reactive.calc
def default_dataset():
    # In browser WebAssembly, return local dataset to prevent CORS downloads
    return pd.DataFrame({
        "duration": [3.6, 1.8, 3.333, 2.283, 4.533, 2.883, 4.7, 3.6, 1.95, 4.35, 1.833, 3.917, 4.2, 1.75, 4.7, 2.167, 1.75, 4.8, 1.6, 4.25],
        "waiting": [79, 54, 74, 62, 85, 55, 88, 85, 51, 85, 54, 84, 78, 47, 83, 52, 62, 84, 52, 79]
    })

@module.server
def hist_server(input, output, session, data_set=default_dataset):
    if data_set is None:
        return None

    @reactive.calc
    def xval():
        return data_set().columns[0]
        
    @reactive.calc
    def datacol():
        return data_set()[xval()]
    
    @render.plot
    def main_plot():
        if not isinstance(data_set(), pd.DataFrame):
            return None
        if data_set().shape[1] < 1:
            return None
        fig, ax = plt.subplots()
        n_breaks = int(input.n_breaks())
        hist_data = np.histogram(datacol(), bins=n_breaks, density=True)
        ax.bar(hist_data[1][:-1], hist_data[0], width=np.diff(hist_data[1]), edgecolor='black', align='edge')

        if input.individual_obs():
            ax.plot(datacol(), np.zeros_like(datacol()), 'r|', markersize=10)

        if input.density():
            bw_adjust = input.bw_adjust()
            kde = gaussian_kde(datacol(), bw_method=bw_adjust)
            x_grid = np.linspace(min(datacol()), max(datacol()), 1000)
            ax.plot(x_grid, kde(x_grid), color='blue')

        ax.set_xlabel(xval())
        ax.set_title(xval())
        return fig
        
    @render.ui
    def output_bw_adjust():
        if input.density():
            return ui.input_slider(
                "bw_adjust",
                "Bandwidth adjustment:",
                min=0.2,
                max=2.0,
                value=1.0,
                step=0.2
            )

@module.ui
def hist_input():
    return ui.card(
        ui.input_select(
            "n_breaks",
            "Number of bins in histogram (approximate):",
            choices=[10, 20, 35, 50],
            selected=20
        ),
        ui.input_checkbox(
            "individual_obs",
            "Show individual observations",
            value=False
        ),
        ui.input_checkbox(
            "density",
            "Show density estimate",
            value=False
        )
    )

@module.ui
def hist_output():
    return ui.output_plot("main_plot")

@module.ui
def hist_ui():
    return ui.output_ui("output_bw_adjust")

Source Code

from shiny import App, render, ui
from geyser.hist import hist_server, hist_input, hist_output, hist_ui
import geyser.io as io

app_ui = ui.page_fluid(
    hist_input("hist"),
    hist_output("hist"),
    hist_ui("hist")
)

def app_server(input, output, session):
    hist_server("hist")

app = App(app_ui, app_server)

io.app_run(app)
from shiny import App, module, reactive, render, ui
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy.stats import gaussian_kde
import geyser.io as io

# Create default reactive for dataset.
@reactive.calc
def default_dataset():
    import seaborn as sns
    return sns.load_dataset("geyser")

@module.server
def hist_server(input, output, session, data_set=default_dataset):
    """Hist Server."""

    if data_set is None:
        return None

    @reactive.calc
    def xval():
        return data_set().columns[0]
    @reactive.calc
    def datacol():
        return data_set()[xval()]
    
    @render.plot
    def main_plot():
        if not isinstance(data_set(), pd.DataFrame):
            return None
        if data_set().shape[1] < 1:
            return None
        fig, ax = plt.subplots()
        n_breaks = int(input.n_breaks())
        hist_data = np.histogram(datacol(), bins=n_breaks, density=True)
        ax.bar(hist_data[1][:-1], hist_data[0], width=np.diff(hist_data[1]), edgecolor='black', align='edge')

        if input.individual_obs():
            ax.plot(datacol(), np.zeros_like(datacol()), 'r|', markersize=10)

        if input.density():
            bw_adjust = input.bw_adjust()
            kde = gaussian_kde(datacol(), bw_method=bw_adjust)
            x_grid = np.linspace(min(datacol()), max(datacol()), 1000)
            ax.plot(x_grid, kde(x_grid), color='blue')

        ax.set_xlabel(xval())
        ax.set_title(xval())
        
    @render.ui
    def output_bw_adjust():
        if input.density():
            return ui.input_slider(
                "bw_adjust",
                "Bandwidth adjustment:",
                min=0.2,
                max=2.0,
                value=1.0,
                step=0.2
            )
    
# hist_server("hist")

@module.ui
def hist_input():
    """Hist Input."""
    return ui.card(
        ui.input_select(
            "n_breaks",
            "Number of bins in histogram (approximate):",
            choices=[10, 20, 35, 50],
            selected=20
        ),
        ui.input_checkbox(
            "individual_obs",
            "Show individual observations",
            value=False
        ),
        ui.input_checkbox(
            "density",
            "Show density estimate",
            value=False
        )
    )

# hist_input("hist")

@module.ui
def hist_output():
    """Hist Output."""
    return ui.output_plot("main_plot")

# hist_output("hist")

@module.ui
def hist_ui():
    """Hist UI."""
    return ui.output_ui("output_bw_adjust")

# hist_ui("hist")

def hist_app():
    """Hist App."""
    app_ui = ui.page_fluid(
        hist_input("hist"),
        hist_output("hist"),
        hist_ui("hist")
    )
    def app_server(input, output, session):
        hist_server("hist")

    app = App(app_ui, app_server)

    #if __name__ == '__main__':
    io.app_run(app)

# hist_app()