Your Next Application
Slide Contents
title: “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}"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.