Dynamic UI
Updating inputs
examples/action-dynamic/slider-min-max/app.py
from shiny import App, ui, reactive
app_ui = ui.page_fluid(
ui.input_numeric("min", "Minimum", 0),
ui.input_numeric("max", "Maximum", 3),
ui.input_slider("n", "n", min=0, max=3, value=1),
)
def server(input, output, session):
@reactive.effect
@reactive.event(input.min)
def _():
ui.update_slider("n", min=input.min())
@reactive.effect
@reactive.event(input.max)
def _():
ui.update_slider("n", max=input.max())
app = App(app_ui, server)Simple uses
examples/action-dynamic/reset/app.py
from shiny import App, ui, reactive
app_ui = ui.page_fluid(
ui.input_slider("x1", "x1", min=-10, max=10, value=0),
ui.input_slider("x2", "x2", min=-10, max=10, value=0),
ui.input_slider("x3", "x3", min=-10, max=10, value=0),
ui.input_action_button("reset", "Reset"),
)
def server(input, output, session):
@reactive.effect
@reactive.event(input.reset)
def _():
ui.update_slider("x1", value=0)
ui.update_slider("x2", value=0)
ui.update_slider("x3", value=0)
app = App(app_ui, server)examples/action-dynamic/action-button-label/app.py
from shiny import App, ui, reactive
app_ui = ui.page_fluid(
ui.input_numeric("n", "Simulations", 10),
ui.input_action_button("simulate", "Simulate"),
)
def server(input, output, session):
@reactive.effect
@reactive.event(input.n)
def _():
label = f"Simulate {input.n()} times"
ui.update_action_button("simulate", label=label)
app = App(app_ui, server)Hierarchical select boxes
examples/action-dynamic/hierarchical-select-box/app.py
from shiny import App, ui, reactive, req, render
import pandas as pd
sales = pd.read_csv("sales-dashboard/sales_data_sample.csv",
sep=",", encoding="Latin-1",
na_values=["", "NaN"], keep_default_na=False)
app_ui = ui.page_fluid(
ui.input_select("territory", "Territory", choices=sales['TERRITORY'].unique().tolist()),
ui.input_select("customername", "Customer", choices=list()),
ui.input_select("ordernumber", "Order number", choices=list()),
ui.output_table("data"),
)
def server(input, output, session):
@reactive.calc
def territory():
return sales[sales['TERRITORY']==input.territory()]
@reactive.effect
@reactive.event(territory)
def _():
choices = territory()['CUSTOMERNAME'].unique().tolist()
ui.update_select("customername", choices=choices)
@reactive.calc
def customer():
req(input.customername())
return territory()[territory()['CUSTOMERNAME']==input.customername()]
@reactive.effect
@reactive.event(customer)
def _():
choices = customer()['ORDERNUMBER'].unique().tolist()
ui.update_select("ordernumber", choices=choices)
@render.table
def data():
req(input.ordernumber())
res = customer()[customer()['ORDERNUMBER']==int(input.ordernumber())]\
[['QUANTITYORDERED', 'PRICEEACH', 'PRODUCTCODE']]
return res
app = App(app_ui, server)Please note that choices=list() was used in ui.input_select() when creating empty drop-down list for select input control. choices=None does not work, because a value of choices must be list, tuple, or dictionary.
Please note that input.ordernumber() returns string, even though underlying data variable ORDERNUMBER is integer. To compare input.ordernumber() with the underlying data variable, I did typecasting int(input.ordernumber()).
Freezing reactive inputs
examples/action-dynamic/freezing-reactive-input/app.py
from shiny import App, ui, reactive, render, req
from pydataset import data
app_ui = ui.page_fluid(
ui.input_select("dataset", "Choose a dataset", choices=("pressure", "cars")),
ui.input_select("column", "Choose column", choices=tuple()),
ui.output_text_verbatim("summary"),
)
def server(input, output, session):
@reactive.calc
def dataset():
return data(input.dataset())
@reactive.effect
@reactive.event(input.dataset)
def _():
reactive.value.freeze(input.column)
ui.update_select("column", choices=dataset().columns.tolist())
@render.text
def summary():
return dataset()[input.column()].describe()
app = App(app_ui, server)Please note that the argument of reactive.value.freeze() is input.column, not input.column().
Exercise
- Let date input allow a user to select only dates in the selected year.
solutions/action-dynamic/updating-inputs/update-year/app.py
from shiny import App, ui, reactive
import datetime
app_ui = ui.page_fluid(
ui.input_numeric("year", "year", value=2020),
ui.input_date("date", "date", value=datetime.date(2020, 1, 1)),
)
def server(input, output, session):
@reactive.effect
@reactive.event(input.year)
def _():
d = datetime.date(
input.year(),
input.date().month,
28 if input.date().month == 2 and input.date().day == 29 else input.date().day
)
ui.update_date("date",
value=d,
min=datetime.date(input.year(), 1, 1),
max=datetime.date(input.year(), 12, 31))
app = App(app_ui, server)- Hierarchical select boxes for state and county
solutions/action-dynamic/updating-inputs/update-county/app.py
# Please download county.csv from https://www.openintro.org/data/
# and store it to data/ folder
# before running this app
from shiny import App, ui, reactive
import pandas as pd
df = pd.read_csv("data/county.csv")
states = df['state'].unique().tolist()
app_ui = ui.page_fluid(
ui.input_select("state", "State", choices=states),
ui.input_select("county", "County", choices=list()),
)
def server(input, output, session):
@reactive.effect
@reactive.event(input.state)
def _():
county_label = "County"
if input.state()=="Louisiana":
county_label = "Parish"
elif input.state()=="Alaska":
county_label = "Borough"
counties = df[df['state']==input.state()]['name'].to_list()
ui.update_select("county", label=county_label, choices=counties)
app = App(app_ui, server)3 & 4.
solutions/action-dynamic/updating-inputs/gapminder/app.py
from shiny import App, ui, reactive, render
from gapminder import gapminder
continents = gapminder['continent'].unique().tolist()
app_ui = ui.page_fluid(
ui.input_select("continent", "Continent", choices=["(All)"] + continents),
ui.input_select("country", "Country", choices=list()),
ui.output_table("data"),
)
def server(input, output, session):
@reactive.effect
@reactive.event(input.continent)
def _():
if input.continent() == "(All)":
countries = gapminder['country'].unique().tolist()
else:
countries = gapminder[gapminder['continent']==input.continent()]['country'].unique().tolist()
ui.update_select("country", choices=countries)
@reactive.calc
def country():
return gapminder[gapminder['country']==input.country()]
@render.table
def data():
return country()
app = App(app_ui, server)Dynamic visibility
examples/action-dynamic/navset/app.py
from shiny import App, ui, reactive
app_ui = ui.page_fluid(
ui.layout_sidebar(
ui.sidebar(
ui.input_select("controller", "Show", choices=[f"panel{x}" for x in range(1, 4)])
),
ui.navset_hidden(
ui.nav_panel("panel1", "Panel 1 content"),
ui.nav_panel("panel2", "Panel 2 content"),
ui.nav_panel("panel3", "Panel 3 content"),
id="switcher",
)
)
)
def server(input, output, session):
@reactive.effect
@reactive.event(input.controller)
def _():
ui.update_navs("switcher", selected=input.controller())
app = App(app_ui, server)Conditional UI
examples/action-dynamic/conditional-ui/app.py
from shiny import App, ui, reactive, render
import numpy as np
import matplotlib.pyplot as plt
parameter_tabs = ui.navset_hidden(
ui.nav_panel("normal",
ui.input_numeric("mean", "mean", value=1),
ui.input_numeric("sd", "standard deviation", min=0, value=1),
),
ui.nav_panel("uniform",
ui.input_numeric("min", "min", value=0),
ui.input_numeric("max", "max", value=1),
),
ui.nav_panel("exponential",
ui.input_numeric("rate", "rate", value=1, min=0)
),
id="params"
)
app_ui = ui.page_fluid(
ui.layout_sidebar(
ui.sidebar(
ui.input_select("dist", "Distribution",
choices=["normal", "uniform", "exponential"]
),
ui.input_numeric("n", "Number of samples", value=100),
parameter_tabs,
),
ui.output_plot("hist"),
)
)
def server(input, output, session):
@reactive.effect
@reactive.event(input.dist)
def _():
ui.update_navs("params", selected=input.dist())
@reactive.calc
def sample():
match input.dist():
case "normal":
res = np.random.normal(input.mean(), input.sd(), input.n())
case "uniform":
res = np.random.uniform(input.min(), input.max(), input.n())
case "exponential":
res = np.random.exponential(1 / input.rate(), input.n())
case _:
res = None
return res
@render.plot
def hist():
plt.hist(sample())
app = App(app_ui, server)Wizard interface
examples/action-dynamic/wizard-interface/app.py
from shiny import App, ui, reactive
app_ui = ui.page_fluid(
ui.navset_hidden(
ui.nav_panel("page_1",
"Welcome!",
ui.input_action_button("page_12", "next")
),
ui.nav_panel("page_2",
"Only one page to go",
ui.input_action_button("page_21", "prev"),
ui.input_action_button("page_23", "next")
),
ui.nav_panel("page_3",
"You're done!",
ui.input_action_button("page_32", "prev")
),
id="wizard"
)
)
def server(input, output, session):
def switch_page(i):
ui.update_navs("wizard", selected=f"page_{i}")
@reactive.effect
@reactive.event(input.page_12)
def _():
switch_page(2)
@reactive.effect
@reactive.event(input.page_21)
def _():
switch_page(1)
@reactive.effect
@reactive.event(input.page_23)
def _():
switch_page(3)
@reactive.effect
@reactive.event(input.page_32)
def _():
switch_page(2)
app = App(app_ui, server)Exercises
- Show additional control only if the user checks an “advanced” checkbox.
solutions/action-dynamic/dynamic-visibility/checkbox-for-control/app.py
from shiny import App, ui, reactive
app_ui = ui.page_fluid(
ui.input_checkbox("advanced", "Advanced"),
ui.navset_hidden(
ui.nav_panel("empty"),
ui.nav_panel("additional",
ui.input_numeric("number", "Additional control",
value=1, min=0, max=10)
),
id="wizard"
)
)
def server(input, output, session):
@reactive.effect
def _():
ui.update_navs("wizard",
selected="additional" if input.advanced() else "empty")
app = App(app_ui, server)- Allow the user to choose which geom to use.
solutions/action-dynamic/diamonds/app.py
from shiny import App, ui, reactive, render
from pydataset import data
from plotnine import ggplot, aes, geom_histogram, geom_freqpoly, geom_density
diamonds = data("diamonds")
app_ui = ui.page_fluid(
ui.input_select("geom", "geom function",
choices=["geom_histogram", "geom_freqpoly", "geom_density"]
),
ui.navset_hidden(
ui.nav_panel("param1",
ui.input_numeric("binwidth", "binwidth", value=0.1, step=0.1),
),
ui.nav_panel("param2",
ui.input_select("bw", "bw",
choices=["nrd0", "normal_reference", "scott", "silverman"],
selected="nrd0"
),
),
id="params",
),
ui.output_plot("plot"),
)
def server(input, output, session):
@reactive.effect
@reactive.event(input.geom)
def _():
match input.geom():
case "geom_histogram":
panel = "param1"
case "geom_freqpoly":
panel = "param1"
case "geom_density":
panel = "param2"
ui.update_navs("params", selected=panel)
@render.plot
def plot():
plot = ggplot(diamonds, aes('carat'))
if input.geom() == "geom_histogram":
res = plot + geom_histogram(binwidth=input.binwidth())
elif input.geom() == "geom_freqpoly":
res = plot + geom_freqpoly(binwidth=input.binwidth())
elif input.geom() == "geom_density":
res = plot + geom_density(bw=input.bw())
return res
app = App(app_ui, server)Creating UI with code
Getting started
examples/action-dynamic/creating-ui/app.py
from shiny import App, ui, reactive, render, req
app_ui = ui.page_fluid(
ui.input_text("label", "label"),
ui.input_select("type", "type", choices=("slider", "numeric")),
ui.output_ui("numeric"),
)
def server(input, output, session):
@render.ui
def numeric():
with reactive.isolate():
try:
value = input.dynamic()
except:
value = 0
if input.type() == "slider":
res = ui.input_slider("dynamic", input.label(), value=value, min=0, max=10)
else:
res = ui.input_numeric("dynamic", input.label(), value=value, min=0, max=10)
return res
app = App(app_ui, server)Simply including the following code did not create UI.
with reactive.isolate():
value = input.dynamic()I guess it is because input.dynamic() sliently throw an error and does not proceed the remaining tasks in the render function. After including explicit error handling with try and except, it performed as desired.
Multiple controls
examples/action-dynamic/color-palette/app.py
from shiny import App, ui, reactive, render
import matplotlib.pyplot as plt
from matplotlib.colors import is_color_like
app_ui = ui.page_fluid(
ui.layout_sidebar(
ui.sidebar(
ui.input_numeric("n", "Number of colours", value=5, min=1),
ui.output_ui("col"),
),
ui.output_plot("plot"),
)
)
def server(input, output, session):
@reactive.calc
def col_names():
return [f"col{x}" for x in range(1, input.n() + 1)]
@render.ui
def col():
with reactive.isolate():
values = {}
for x in col_names():
try:
values[x] = input[x]()
except:
values[x] = ""
return [ui.input_text(x, None, value=values[x]) for x in col_names()]
@reactive.calc
def palette():
return [input[x]() for x in col_names()]
@render.plot
def plot():
cols = [x if is_color_like(x) else "none" for x in palette()]
fig, ax = plt.subplots()
ax.bar(x=col_names(), height=1, width=1, color=cols, edgecolor='grey')
ax.set_axis_off()
return ax
app = App(app_ui, server)Dynamic filtering
examples/action-dynamic/dynamic-filtering/app.py
from shiny import App, ui, reactive, render
import numpy as np
import pandas as pd
from pandas.api.types import is_numeric_dtype, is_string_dtype
from pydataset import data
def make_ui(x, var):
if is_numeric_dtype(x):
rng = (x.min(), x.max())
res = ui.input_slider(var, var, min=rng[0], max=rng[1], value=rng)
elif is_string_dtype(x):
levs = list(np.unique(x))
res = ui.input_select(var, var, choices=levs, selected=levs, multiple=True)
else:
res = None
return res
def filter_var(x, val):
if is_numeric_dtype(x):
res = (~np.isnan(x)) & (x >= val[0]) & (x <= val[1])
elif is_string_dtype(x):
res = np.isin(x, val)
else:
res = True
return res
iris = data('iris')
app_ui = ui.page_fluid(
ui.layout_sidebar(
ui.sidebar(
make_ui(iris['Sepal.Length'], "Sepal_Length"),
make_ui(iris['Sepal.Width'], "Sepal_Width"),
make_ui(iris['Species'], "Species"),
),
ui.output_table("data"),
)
)
def server(input, output, session):
@reactive.calc
def selected():
res = filter_var(iris['Sepal.Length'], input['Sepal_Length']()) & \
filter_var(iris['Sepal.Width'], input['Sepal_Width']()) &\
filter_var(iris['Species'], input['Species']())
return res
@render.table
def data():
return iris[selected()].head(12)
app = App(app_ui, server)Shiny for Python does not allow period(.) be a part of input ID. Therefore, I replaced . with _ when creating input control while let column names still have a period.
This makes it difficult to create more generalized app that filter any (even user-uploaded) data with any column name type, as shown in Mastering Shiny book for R.
More generalized data is as follows:
examples/action-dynamic/dynamic-filtering/app_generalized.py
from shiny import App, ui, reactive, render
import numpy as np
import pandas as pd
from pandas.api.types import is_numeric_dtype, is_string_dtype
from pydataset import data
import janitor
from functools import reduce
dfs = data()['dataset_id'].to_list()
def make_ui(x, var):
if is_numeric_dtype(x):
rng = (x.min(), x.max())
res = ui.input_slider(var, var, min=rng[0], max=rng[1], value=rng)
elif is_string_dtype(x):
levs = list(np.unique(x))
res = ui.input_select(var, var, choices=levs, selected=levs, multiple=True)
else:
res = None
return res
def filter_var(x, val):
if is_numeric_dtype(x):
res = (~np.isnan(x)) & (x >= val[0]) & (x <= val[1])
elif is_string_dtype(x):
res = np.isin(x, val)
else:
res = True
return res
app_ui = ui.page_fluid(
ui.layout_sidebar(
ui.sidebar(
ui.input_select("_dataset", label="Dataset", choices=dfs),
ui.output_ui("_filter"),
),
ui.output_table("_data"),
)
)
def server(input, output, session):
@reactive.calc
def cleaned_data():
return data(input._dataset()).clean_names(case_type='snake')
@reactive.calc
def _vars():
return cleaned_data().columns
@render.ui
def _filter():
return list(map(lambda x: make_ui(cleaned_data()[x], x), _vars())),
@reactive.calc
def selected():
each_var = map(lambda x: filter_var(cleaned_data()[x], input[x]()), _vars())
res = reduce(lambda x, y: x & y, each_var)
return res
@render.table
def _data():
return cleaned_data()[selected()].head(12)
app = App(app_ui, server)I cleaned column names by using {pyjanitor} package before dynamically creating UIs. This was to automate UI creation based on column names.
Also, I placed a leading underscore(_) for static UI’s ID, to reduce a chance that dynamic UI’s ID conflict with static UI’s ID.
I used is_string_dtype() to check whether the column is for categorical variable because data columns have not been converted to categorical variable yet. It would be better to consider converting columns to Categorical dtype first and use a function is_categorical_dtype() or its equivalent.
Excercises
solutions/action-dynamic/creating-ui/keep-values-in-sync/app.py
from shiny import App, reactive, render, req, ui
app_ui = ui.page_fluid(
ui.input_select("type", "type", ["slider", "numeric"]),
ui.navset_hidden(
ui.nav_panel("slider",
ui.input_slider("n_slider", "n", value=0, min=0, max=100)),
ui.nav_panel("numeric",
ui.input_numeric("n_numeric", "n", value=0, min=0, max=100)),
id="wizard",
),
)
def server(input, output, session):
@reactive.effect
def _():
ui.update_navs("wizard", selected=input.type())
@reactive.effect
@reactive.event(input.n_slider)
def _():
ui.update_numeric("n_numeric", value=input.n_slider())
@reactive.effect
@reactive.event(input.n_numeric)
def _():
ui.update_slider("n_slider", value=input.n_numeric())
app = App(app_ui, server)When switching from numeric input control to slider input control, select value of slider input appears to be 0 for <1 second prior to automatically showing a synced value. I hope to find a solution to avoid this issue.
solutions/action-dynamic/creating-ui/passwrd-dialog/app.py
from shiny import App, reactive, render, req, ui
app_ui = ui.page_fluid(
ui.input_action_button("go", "Enter password"),
ui.output_text("text"),
)
def server(input, output, session):
pw = reactive.value('')
@reactive.effect
@reactive.event(input.go)
def _():
m = ui.modal(
ui.input_password("password", "Password: ", value=pw.get()),
ui.input_action_button("submit", "Submit"),
title="Please enter your password",
footer=None,
)
ui.modal_show(m)
@reactive.calc
@reactive.event(input.submit)
def get_password():
res = input.password()
pw.set(res)
ui.modal_remove()
return res
@render.text
def text():
if get_password()=="":
return "No password"
return "Password entered"
app = App(app_ui, server)solutions/action-dynamic/creating-ui/dynamic-filtering-date/app.py
from shiny import App, ui, reactive, render
import numpy as np
import pandas as pd
from pandas.api.types import is_numeric_dtype, is_string_dtype, is_datetime64_dtype
import janitor
from functools import reduce
from datetime import datetime
data = pd.DataFrame({
'date': pd.to_datetime(['2021-01-01', '2021-01-02', '2021-01-03']).to_pydatetime(),
'datetime': pd.to_datetime(['2021-01-01 00:00:00', '2021-01-02 01:00:00', '2021-01-03 23:59:59'])
})
def make_ui(x, var):
if is_numeric_dtype(x):
rng = (x.min(), x.max())
res = ui.input_slider(var, var, min=rng[0], max=rng[1], value=rng)
elif is_string_dtype(x):
levs = list(np.unique(x))
res = ui.input_select(var, var, choices=levs, selected=levs, multiple=True)
elif is_datetime64_dtype(x):
rng = (x.min().to_pydatetime().date(), x.max().to_pydatetime().date())
res = ui.input_date_range(var, var, min=rng[0], max=rng[1], start=rng[0], end=rng[1])
else:
res = None
return res
def filter_var(x, val):
if is_numeric_dtype(x):
res = (~np.isnan(x)) & (x >= val[0]) & (x <= val[1])
elif is_string_dtype(x):
res = np.isin(x, val)
elif is_datetime64_dtype(x):
res = (~np.isnan(x)) & \
(x >= datetime.combine(val[0], datetime.min.time())) & \
(x <= datetime.combine(val[1], datetime.max.time()))
else:
res = True
return res
app_ui = ui.page_fluid(
ui.layout_sidebar(
ui.sidebar(
ui.output_ui("_filter"),
),
ui.output_table("_data"),
)
)
def server(input, output, session):
@render.ui
def _filter():
return list(map(lambda x: make_ui(data[x], x), data.columns))
@reactive.calc
def selected():
each_var = map(lambda x: filter_var(data[x], input[x]()), data.columns)
res = reduce(lambda x, y: x & y, each_var)
return res
@render.table
def _data():
return data[selected()].head(12)
app = App(app_ui, server)