Your Next Application

Beyond the basics

Your first app had one input and one output.

Real dashboards have:

  • Layout structure (sidebars, columns, tabs)
    • shiny.express.ui.*()
  • Multiple coordinated outputs
  • Summary statistics (value boxes)
    • ui.value_box()
  • Shared reactive computations
    • @reactive.calc
  • Interactive plots (jupyter widgets)
    • shinywidgets.render_*()

Example application

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

import plotly.express as px
from ridgeplot import ridgeplot
import seaborn as sns
from shiny.express import input, ui, render
from shiny import reactive
from shinywidgets import render_plotly, render_widget

tips = sns.load_dataset("tips")

# title
ui.page_opts(title="Restaurant tipping", fillable=True)

# sidebar
with ui.sidebar(open="desktop"):
    ui.input_slider(
        id="slider",
        label="Bill amount",
        min=tips.total_bill.min(),
        max=tips.total_bill.max(),
        value=[tips.total_bill.min(), tips.total_bill.max()],
    )
    ui.input_checkbox_group(
        id="checkbox_group",
        label="Food service",
        choices={
            "Lunch": "Lunch",
            "Dinner": "Dinner",
        },
        selected=[
            "Lunch",
            "Dinner",
        ],
    )
    ui.input_action_button("action_button", "Reset filter")


@reactive.effect
@reactive.event(input.action_button)
def reset_filters():
    ui.update_slider(
        "slider",
        value=[tips.total_bill.min(), tips.total_bill.max()],
    )
    ui.update_checkbox_group(
        "checkbox_group",
        selected=["Lunch", "Dinner"],
    )


@reactive.calc
def filtered_data():
    idx1 = tips.total_bill.between(
        left=input.slider()[0],
        right=input.slider()[1],
        inclusive="both",
    )
    idx2 = tips.time.isin(input.checkbox_group())
    tips_filtered = tips[idx1 & idx2]
    return tips_filtered


# body of application
# first row of value boxes
with ui.layout_columns(fill=False):
    with ui.value_box():
        "Total tippers"

        @render.text
        def total_tippers():
            return filtered_data().shape[0]

    with ui.value_box():
        "Average tip"

        @render.text
        def average_tip():
            perc = filtered_data().tip / filtered_data().total_bill
            return f"{perc.mean():.1%}"

    with ui.value_box():
        "Average bill"

        @render.text
        def average_bill():
            bill = filtered_data().total_bill.mean()
            return f"${bill:.2f}"


# second row of cards
with ui.layout_columns(col_widths=[6, 6]):
    with ui.card(full_screen=True):
        ui.card_header("Tips data")

        @render.data_frame
        def tips_data():
            return filtered_data()

    with ui.card(full_screen=True):
        ui.card_header("Total bill vs tip")

        @render_plotly
        def scatterplot():
            return px.scatter(
                filtered_data(), x="total_bill", y="tip", trendline="lowess"
            )


with ui.layout_columns():
    with ui.card(full_screen=True):
        ui.card_header("Tip percentages")

        @render_widget
        def ridge():
            filtered_data()["percent"] = (
                filtered_data().tip / filtered_data().total_bill
            )

            uvals = filtered_data().day.unique()
            samples = [
                [filtered_data().percent[filtered_data().day == val]] for val in uvals
            ]

            plt = ridgeplot(
                samples=samples,
                labels=uvals,
                bandwidth=0.01,
                colorscale="viridis",
                colormode="row-index",
            )

            plt.update_layout(
                legend=dict(
                    orientation="h", yanchor="bottom", y=1.02, xanchor="center", x=0.5
                )
            )

            return plt


## file: requirements.txt
ridgeplot

Layout components

from shiny.express import input, render, ui

ui.page_opts(title="Tips Dashboard", fillable=True)

with ui.sidebar():
    ui.input_slider("bill", "Max bill ($)", 3, 50, 25)
    ui.input_checkbox_group(
        "time",
        "Meal",
        ["Lunch", "Dinner"],
        selected=["Lunch", "Dinner"],
    )
    ui.input_action_button("action_button", "Reset")

with ui.layout_columns():
    with ui.card():
        ...  # output 1

    with ui.card():
        ...  # output 2

    with ui.card():
            ...  # output 3

with ui.layout_columns():
    with ui.card():
        ...  # data frame

    with ui.card():
        ...  # scatter plot

with ui.layout_columns():
    with ui.card():
        ...  # ridgeplot

Value boxes

https://shiny.posit.co/py/components/outputs/value-box/

from shiny.express import render, ui
from faicons import icon_svg

with ui.value_box(showcase=icon_svg("dollar-sign")):
    "Average bill"

    @render.text
    def avg_bill():
        return f"${filtered()['total_bill'].mean():.2f}"

@reactive.calc for shared filtering

https://shiny.posit.co/py/docs/reactive-foundations.html#calculations

from shiny import reactive

@reactive.calc
def filtered():
    df = tips.copy()
    df = df[df["total_bill"] <= input.bill()]
    df = df[df["time"].isin(input.time())]
    return df

All outputs call filtered() — the filter runs once per change.

Full app structure

ui.page_opts(title="Tips Dashboard", fillable=True)

with ui.sidebar():
    # inputs ...

@reactive.calc
def filtered():
    # shared data transform ...

with ui.layout_columns():
    with ui.value_box(...):
        @render.text
        def avg_bill(): ...

    with ui.card():
        @render.plot
        def scatter(): ...

    with ui.card():
        @render.data_frame
        def table(): ...

Pausing computations

Sometimes you want an action to wait until you say so (e.g., do something when I push a button)

  • Using user text input to filter
  • Adjusting multiple filters

We can use a ui.innput_action_buttion() input component to trigger a reactive event.

https://shiny.posit.co/py/components/inputs/action-button/

Parts of an action button

  1. UI input component: ui.innput_action_buttion()
  2. A function that gets called when the button gets clicked
    • @reactive.event(input.<ID>) decorator which button triggers which function
    • @reactive.effect decorator a button click doesn’t return a result, it creates a side effect

Exercise