Tips Dashboard


title: Build a dashboard

What do you see?

Restaurant Tipping dashboard

  1. Title: “Restaurant tipping”

. . .

  1. Sidebar for a few input components (we’ll add those later)
    • You can put some text here as a place holder, e.g., "sidebar inputs"

. . .

  1. A full width column with 3 value boxes
    • Each value box will take up the same width of space
    • The value boxes will have separate labels and corresponding summary statistic

. . .

  1. A full width column with 2 cards, one for a dataframe and another for a scatter plot
    • Each card will share the same width of space

. . .

  1. A full width column with 1 card

App UI components

Here are the documentation pages for functions that may be useful for this exercise:

Core API docs: https://shiny.posit.co/py/api/core/

App UI

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

from shiny import App, ui

# UI
app_ui = ui.page_fillable(
    ui.panel_title("Restaurant tipping"),
    ui.layout_sidebar(
        ui.sidebar("sidebar inputs", open="desktop"),
        ui.layout_columns(
            ui.value_box("Total tippers", "Value 1"),
            ui.value_box("Average tip", "Value 2"),
            ui.value_box("Average bill", "Value 3"),
            fill=False,
        ),
        ui.layout_columns(
            ui.card(ui.card_header("Tips data"), full_screen=True),
            ui.card(ui.card_header("Total bill vs tip"), full_screen=True),
            col_widths=[6, 6],
        ),
        ui.layout_columns(ui.card(ui.card_header("Tip percentages"), full_screen=True)),
    ),
)


# Server
def server(input, output, session):
    pass


# Create app
app = App(app_ui, server)

Add input components

from shiny import App, ui

app_ui = ui.page_fillable(
    ui.panel_title("Restaurant tipping"),
    ui.layout_sidebar(
        ui.sidebar(
            ui.input_slider(
                id="slider",
                label="Bill amount",
                min=0,
                max=100,
                value=[0, 100],
            ),
            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"),
            open="desktop",
        ),
        ...
    ),
)

def server(input, output, session):
    pass

app = App(app_ui, server)
#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [viewer]
#| viewerHeight: 500

from shiny import App, ui

app_ui = ui.page_fillable(
    ui.panel_title("Restaurant tipping"),
    ui.layout_sidebar(
        ui.sidebar(
            ui.input_slider(
                id="slider",
                label="Bill amount",
                min=0,
                max=100,
                value=[0, 100],
            ),
            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"),
            open="desktop",
        ),
        ui.layout_columns(
            ui.value_box("Total tippers", "Value 1"),
            ui.value_box("Average tip", "Value 2"),
            ui.value_box("Average bill", "Value 3"),
            fill=False,
        ),
        ui.layout_columns(
            ui.card(ui.card_header("Tips by day"), full_screen=True),
            ui.card(ui.card_header("Total bill vs tip"), full_screen=True),
            col_widths=[6, 6],
        ),
        ui.layout_columns(
            ui.card(ui.card_header("Tip percentages"), full_screen=True),
        ),
    ),
)

def server(input, output, session):
    pass

app = App(app_ui, server)

Let’s add some data

import seaborn as sns

# load the tips dataset
tips = sns.load_dataset('tips')

# default values from data (will come from widgets later)
total_lower = tips.total_bill.min()
total_upper = tips.total_bill.max()
time_selected = tips.time.unique().tolist()

# filter by bill amount range
idx1 = tips.total_bill.between(
    left=total_lower,
    right=total_upper,
    inclusive="both",
)

# filter by meal time
idx2 = tips.time.isin(time_selected)

# combine filters
tips_filtered = tips[idx1 & idx2]

Calculate values

# tips_filtered is the dataset after applying
# slider (bill range) and checkbox (meal time) filters.
# These will be the values for our 3 value boxes.

# total tippers
total_tippers = tips_filtered.shape[0]
print(total_tippers)

# average tip as a percentage of the bill
perc = tips_filtered.tip / tips_filtered.total_bill
average_tip = f"{perc.mean():.1%}"
print(average_tip)

# average bill formatted as currency
bill = tips_filtered.total_bill.mean()
average_bill = f"${bill:.2f}"
print(average_bill)
244
16.1%
$19.79

Add output placeholders to the UI

Before: static strings

ui.value_box("Total tippers", "Value 1"),
ui.value_box("Average tip", "Value 2"),
ui.value_box("Average bill", "Value 3"),

After: output placeholders — Shiny will fill these from the server (loading until we wire them up)

ui.value_box("Total tippers", ui.output_text("total_tippers")),
ui.value_box("Average tip", ui.output_text("average_tip")),
ui.value_box("Average bill", ui.output_text("average_bill")),
#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [viewer]
#| viewerHeight: 500

import seaborn as sns
from shiny import App, render, ui

tips = sns.load_dataset("tips")

app_ui = ui.page_fillable(
    ui.panel_title("Restaurant tipping"),
    ui.layout_sidebar(
        ui.sidebar(
            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"),
            open="desktop",
        ),
        ui.layout_columns(
            ui.value_box("Total tippers", ui.output_text("total_tippers")),
            ui.value_box("Average tip", ui.output_text("average_tip")),
            ui.value_box("Average bill", ui.output_text("average_bill")),
            fill=False,
        ),
    ),
)

def server(input, output, session):
    pass

app = App(app_ui, server)

Wire up the server

Before: empty server

def server(input, output, session):
    pass

After: @render.text function name matches ui.output_text("id")

def server(input, output, session):
    @render.text
    def total_tippers():
        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 str(tips_filtered.shape[0])

    @render.text
    def average_tip():
        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 f"{(tips_filtered.tip / tips_filtered.total_bill).mean():.1%}"

    @render.text
    def average_bill():
        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 f"${tips_filtered.total_bill.mean():.2f}"
#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [viewer]
#| viewerHeight: 600

import seaborn as sns
from shiny import App, reactive, render, ui

tips = sns.load_dataset("tips")

app_ui = ui.page_fillable(
    ui.panel_title("Restaurant tipping"),
    ui.layout_sidebar(
        ui.sidebar(
            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"),
            open="desktop",
        ),
        ui.layout_columns(
            ui.value_box("Total tippers", ui.output_text("total_tippers")),
            ui.value_box("Average tip", ui.output_text("average_tip")),
            ui.value_box("Average bill", ui.output_text("average_bill")),
            fill=False,
        ),
    ),
)


def server(input, output, session):
    @render.text
    def total_tippers():
        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 str(tips_filtered.shape[0])

    @render.text
    def average_tip():
        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]
        perc = tips_filtered.tip / tips_filtered.total_bill
        return f"{perc.mean():.1%}"

    @render.text
    def average_bill():
        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]
        bill = tips_filtered.total_bill.mean()
        return f"${bill:.2f}"


app = App(app_ui, server)
# UI placeholder inside the card
ui.card(
    ui.card_header("Tip percentages"),
    output_widget("ridge"),
    full_screen=True,
),
# Server function — same ridgeplot code, now using filtered_data()
@render_widget
def ridge():
    df = filtered_data().copy()
    df["percent"] = df.tip / df.total_bill

    uvals = df.day.unique()
    samples = [[df.percent[df.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

The application (Core)

#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [editor, viewer]
#| layout: horizontal
#| viewerHeight: 500
from shiny import App, reactive, render, ui

app_ui = ui.page_fillable(
    ui.input_slider("bill", "Bill amount ($)", min=1, max=100, value=25),
    ui.output_text("tip_inline"),
    ui.output_text("tip_from_calc"),
)


def server(input, output, session):
    @render.text
    def tip_inline():
        """Computed directly from input."""
        return f"Tip (15%) [tip_inline]: ${input.bill() * 0.15:.2f}"

    @reactive.calc
    def tip_amount():
        """Saved for reuse."""
        return input.bill() * 0.15

    @render.text
    def tip_from_calc():
        """Uses saved @reactive.calc."""
        return f"Tip (saved) [tip_from_calc]: ${tip_amount():.2f}"


app = App(app_ui, server)
from shiny import App, render, ui, reactive
import plotly.express as px
from ridgeplot import ridgeplot
import seaborn as sns
from shinywidgets import render_plotly, render_widget, output_widget

tips = sns.load_dataset("tips")

# UI
app_ui = ui.page_fluid(
    ui.tags.style("body { font-size: 0.6em; }"),
    ui.panel_title("Restaurant tipping"),
    ui.layout_sidebar(
        ui.sidebar(
            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"),
            open="desktop",
        ),
        ui.layout_columns(
            ui.value_box("Total tippers", ui.output_text("total_tippers")),
            ui.value_box("Average tip", ui.output_text("average_tip")),
            ui.value_box("Average bill", ui.output_text("average_bill")),
            fill=False,
        ),
        ui.layout_columns(
            ui.card(
                ui.card_header("Tips by day"),
                ui.output_data_frame("tips_data"),
                full_screen=True,
            ),
            ui.card(
                ui.card_header("Total bill vs tip"),
                output_widget("scatterplot"),
                full_screen=True,
            ),
            col_widths=[6, 6],
        ),
        ui.layout_columns(
            ui.card(
                ui.card_header("Tip percentages"),
                output_widget("ridge"),
                full_screen=True,
            )
        ),
    ),
)

[`tip_amount()` is computed once and cached - multiple outputs (`tip_from_calc`, `total_from_calc`) reuse the same result instead of recalculating]{style="font-size: 0.6em;"}

```{shinylive-python}
#| standalone: true
#| components: [editor, viewer]
#| layout: horizontal
#| viewerHeight: 500
from shiny import App, reactive, render, ui

app_ui = ui.page_fillable(
    ui.input_slider("bill", "Bill amount ($)", min=1, max=100, value=25),
    ui.output_text("tip_inline"),
    ui.output_text("tip_from_calc"),
    ui.output_text("total_from_calc"),
    ui.output_text("total_from_input"),
)


def server(input, output, session):
    @render.text
    def tip_inline():
        """Computed directly from input."""
        return f"Tip (15%) [tip_inline]: ${input.bill() * 0.15:.2f}"

    @reactive.calc
    def tip_amount():
        """Saved for reuse."""
        return input.bill() * 0.15

    @render.text
    def tip_from_calc():
        """Uses saved @reactive.calc."""
        return f"Tip (saved) [tip_from_calc]: ${tip_amount():.2f}"

    @render.text
    def total_from_calc():
        """Builds on saved calc."""
        return f"Total with tip [total_from_calc]: ${input.bill() + tip_amount():.2f}"

    @render.text
    def total_from_input():
        """Recalculates from scratch."""
        return f"Total (recalc) [total_from_input]: ${input.bill() + input.bill() * 0.15:.2f}"


app = App(app_ui, server)

Apply @reactive.calc to the dashboard

Same pattern as tip_amount() — filter once, reuse in all value boxes

Before: filtering repeated in every render function

def server(input, output, session):
    @render.text
    def total_tippers():
        idx1 = tips.total_bill.between(...)
        idx2 = tips.time.isin(...)
        tips_filtered = tips[idx1 & idx2]
        return str(tips_filtered.shape[0])

    @render.text
    def average_tip():
        idx1 = tips.total_bill.between(...)  # same
        idx2 = tips.time.isin(...)           # same
        tips_filtered = tips[idx1 & idx2]    # same
        ...

After: extract @reactive.calc, each render just uses filtered_data()

def server(input, output, session):
    @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())
        return tips[idx1 & idx2]

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

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

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

Dashboard with @reactive.calc

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

import seaborn as sns
from shiny import App, reactive, render, ui

tips = sns.load_dataset("tips")

app_ui = ui.page_fillable(
    ui.panel_title("Restaurant tipping"),
    ui.layout_sidebar(
        ui.sidebar(
            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"),
            open="desktop",
        ),
        ui.layout_columns(
            ui.value_box("Total tippers", ui.output_text("total_tippers")),
            ui.value_box("Average tip", ui.output_text("average_tip")),
            ui.value_box("Average bill", ui.output_text("average_bill")),
            fill=False,
        ),
    ),
)

def server(input, output, session):
    @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())
        return tips[idx1 & idx2]

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

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

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

app = App(app_ui, server)

Add data frame

/tmp/ipykernel_2741/3163269639.py:3: FutureWarning: The default of observed=False is deprecated and will be changed to True in a future version of pandas. Pass observed=False to retain current behavior or observed=True to adopt the future default and silence this warning.
  df.groupby("day").agg(
day count avg_bill avg_tip avg_tip_pct
0 Thur 62 17.68 2.77 0.16
1 Fri 19 17.15 2.73 0.17
2 Sat 87 20.44 2.99 0.15
3 Sun 76 21.41 3.26 0.17

UI: ui.output_data_frame("id") — Server: @render.data_frameMutable objects

# UI placeholder inside the card
ui.card(
    ui.card_header("Tips by day"),
    ui.output_data_frame("tips_data"),
    full_screen=True,
),
# Server function — aggregate by day
@render.data_frame
def tips_data():
    df = filtered_data().copy()
    df["tip_pct"] = df.tip / df.total_bill
    summary = df.groupby("day").agg(
        count=("tip", "size"),
        avg_bill=("total_bill", "mean"),
        avg_tip=("tip", "mean"),
        avg_tip_pct=("tip_pct", "mean"),
    ).round(2).reset_index()
    return summary

Scatterplot

Relationship between bill amount and tip with a smoothed trend

import plotly.express as px

px.scatter(
    tips_filtered,
    x="total_bill",
    y="tip",
    trendline="lowess"
)

Add the scatterplot

UI: output_widget("id") — Server: @render_plotly (from shinywidgets)

from shinywidgets import render_plotly, output_widget
# UI placeholder inside the card
ui.card(
    ui.card_header("Total bill vs tip"),
    output_widget("scatterplot"),
    full_screen=True,
),
# Server function — same px.scatter(), now using filtered_data()
@render_plotly
def scatterplot():
    return px.scatter(filtered_data(), x="total_bill", y="tip", trendline="lowess")

Ridgeplot

Distribution of tip percentages broken out by day of the week

from ridgeplot import ridgeplot

tips_filtered["percent"] = tips_filtered.tip / tips_filtered.total_bill

uvals = tips_filtered.day.unique()
samples = [[tips_filtered.percent[tips_filtered.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
    )
)

Add the ridgeplot

UI: output_widget("id") — Server: @render_widget (from shinywidgets)

from shinywidgets import render_widget, output_widget
# UI placeholder inside the card
ui.card(
    ui.card_header("Tip percentages"),
    output_widget("ridge"),
    full_screen=True,
),
# Server function — same ridgeplot code, now using filtered_data()
@render_widget
def ridge():
    df = filtered_data().copy()
    df["percent"] = df.tip / df.total_bill

    uvals = df.day.unique()
    samples = [[df.percent[df.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

The application (Core)

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

## file: requirements.txt
ridgeplot
shinywidgets
statsmodels

## file: app.py
from shiny import App, render, ui, reactive
import plotly.express as px
from ridgeplot import ridgeplot
import seaborn as sns
from shinywidgets import render_plotly, render_widget, output_widget

tips = sns.load_dataset("tips")

# UI
app_ui = ui.page_fluid(
    ui.tags.style("body { font-size: 0.6em; }"),
    ui.panel_title("Restaurant tipping"),
    ui.layout_sidebar(
        ui.sidebar(
            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"),
            open="desktop",
        ),
        ui.layout_columns(
            ui.value_box("Total tippers", ui.output_text("total_tippers")),
            ui.value_box("Average tip", ui.output_text("average_tip")),
            ui.value_box("Average bill", ui.output_text("average_bill")),
            fill=False,
        ),
        ui.layout_columns(
            ui.card(
                ui.card_header("Tips by day"),
                ui.output_data_frame("tips_data"),
                full_screen=True,
            ),
            ui.card(
                ui.card_header("Total bill vs tip"),
                output_widget("scatterplot"),
                full_screen=True,
            ),
            col_widths=[6, 6],
        ),
        ui.layout_columns(
            ui.card(
                ui.card_header("Tip percentages"),
                output_widget("ridge"),
                full_screen=True,
            )
        ),
    ),
)


# Server
def server(input, output, session):

    @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

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

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

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

    @render.data_frame
    def tips_data():
        df = filtered_data().copy()
        df["tip_pct"] = df.tip / df.total_bill
        summary = df.groupby("day").agg(
            count=("tip", "size"),
            avg_bill=("total_bill", "mean"),
            avg_tip=("tip", "mean"),
            avg_tip_pct=("tip_pct", "mean"),
        ).round(2).reset_index()
        return summary

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

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

        uvals = df.day.unique()
        samples = [[df.percent[df.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


# Create app
app = App(app_ui, server)

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

tips = sns.load_dataset("tips")

# UI
app_ui = ui.page_fluid(
    ui.tags.style("body { font-size: 0.6em; }"),
    ui.panel_title("Restaurant tipping"),
    ui.layout_sidebar(
        ui.sidebar(
            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"),
            open="desktop",
        ),
        ui.layout_columns(
            ui.value_box("Total tippers", ui.output_text("total_tippers")),
            ui.value_box("Average tip", ui.output_text("average_tip")),
            ui.value_box("Average bill", ui.output_text("average_bill")),
            fill=False,
        ),
        ui.layout_columns(
            ui.card(
                ui.card_header("Tips by day"),
                ui.output_data_frame("tips_data"),
                full_screen=True,
            ),
            ui.card(
                ui.card_header("Total bill vs tip"),
                output_widget("scatterplot"),
                full_screen=True,
            ),
            col_widths=[6, 6],
        ),
        ui.layout_columns(
            ui.card(
                ui.card_header("Tip percentages"),
                output_widget("ridge"),
                full_screen=True,
            )
        ),
    ),
)


# Server
def server(input, output, session):

    @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

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

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

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

    @render.data_frame
    def tips_data():
        df = filtered_data().copy()
        df["tip_pct"] = df.tip / df.total_bill
        summary = df.groupby("day").agg(
            count=("tip", "size"),
            avg_bill=("total_bill", "mean"),
            avg_tip=("tip", "mean"),
            avg_tip_pct=("tip_pct", "mean"),
        ).round(2).reset_index()
        return summary

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

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

        uvals = df.day.unique()
        samples = [[df.percent[df.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


# Create app
app = App(app_ui, server)

What we covered

Shiny for Python docs: https://shiny.posit.co/py/