User Feedback
Validation
Validating input
This requires a pacakge shiny_validate, which is a python implementation of R package shinyvalidate.
action-feedback/input-validation/app.py
from shiny import App, ui, render, reactive, req
from shiny_validate import InputValidator
app_ui = ui.page_fluid(
ui.input_numeric("n", "n", value=10),
ui.output_text("half"),
)
def server(input, output, session):
iv = InputValidator()
iv.add_rule("n", lambda x: "Please select an even number" if x%2 != 0 else None)
iv.enable()
@render.text
def half():
even = input.n()%2 == 0
req(even)
return input.n()/2
app = App(app_ui, server)Depending on {shiny_validate} version, it may reinstall {shiny} with different version. Please choose a right version that is compatible with your shiny installation or consider pip install --no-deps when installing {shiny_validate}.
When there are mutliple controls and/or multiple rules, you can use iv.is_valid() to check whether all the input validation rules currently pass.
Cancelling execution with req()
examples/action-feedback/cancelling-execution/app.py
from shiny import App, ui, render, req
app_ui = ui.page_fluid(
ui.input_select("language", "Language", choices=["", "English", "Maori"]),
ui.input_text("name", "Name"),
ui.output_text("greeting"),
)
def server(input, output, session):
greetings = {
'English': "Hello",
'Maori': "Ki ora",
}
@render.text
def greeting():
req(input.language(), input.name())
return f"{greetings[input.language()]} {input.name()}!"
app = App(app_ui, server)req() and validation
Below is my attempt to implement an app that behave the same to the Mastering Shiny book example:
- When no input is provided, does not raise an error and just does not show any table
- When invalid data name is provided, show an error message, but keep a table output to be a table associated with the last valid input.
examples/action-feedback/dataset-name/app.py
from shiny import App, ui, render, reactive, req
from shiny_validate import InputValidator
from pydataset import data
import numpy as np
datasets = set(data()['dataset_id'])
app_ui = ui.page_fluid(
ui.input_text("dataset", "Dataset name"),
ui.output_table("table"),
)
def server(input, output, session):
iv = InputValidator()
iv.add_rule("dataset",
lambda x: "Unknown dataset" \
if len(x) > 0 and not x in datasets \
else None)
iv.enable()
@reactive.calc
def load():
req(input.dataset())
req(iv.is_valid(), cancel_output=True)
return data(input.dataset())
@render.table
def table():
return load().head()
app = App(app_ui, server)Below is slightly different implementation by using check submodule of {shiny_validate}.
examples/action-feedback/dataset-name/app-check.py
from shiny import App, ui, render, reactive, req
from shiny_validate import InputValidator, check
from pydataset import data
import numpy as np
datasets = set(data()['dataset_id'])
app_ui = ui.page_fluid(
ui.input_text("dataset", "Dataset name"),
ui.output_table("table"),
)
def server(input, output, session):
iv = InputValidator()
iv.add_rule("dataset",
check.compose_rules(
check.required("Empty dataset name"),
check.in_set(datasets, "Unknown dataset")),
)
iv.enable()
@reactive.calc
def load():
req(input.dataset())
req(iv.is_valid(), cancel_output=True)
return data(input.dataset())
@render.table
def table():
return load().head()
app = App(app_ui, server)check.required()is to set the input as required and throw an error message when input has not been provided.check.in_set()is to check whether the input is an element of givenset. It is important that the type ofsetargument should beset.check.compose_rules()is to add multiple rules to a single input control.
Validate output
I could not find Shiny for Python’s equivalent of Shiny for R’s validate() function. Implementation below is per Gorden Shotwell’s recommendation on this thread.
examples/action-feedback/output-validation/app.py
from shiny import App, ui, render, reactive, req
import numpy as np
app_ui = ui.page_fluid(
ui.input_numeric("x", "x", value=0),
ui.input_select("trans", "transformation", choices=["square", "log", "square-root"]),
ui.output_ui("out_container"),
)
def server(input, output, session):
@render.ui
def out_container():
if input.x() < 0 and input.trans() in ["log", "square-root"]:
return ui.markdown("**x can not be negative for this transformation**")
else:
return ui.output_text("out")
@render.text
def out():
req(not (input.x() < 0 and input.trans() in ["log", "square-root"]))
match input.trans():
case "square":
res = input.x()**2
case "square-root":
res = np.sqrt(input.x())
case "log":
res = np.log(input.x())
return res
app = App(app_ui, server)I could not find Shiny for Python’s equivalent of Shiny for R’s validate() function. Implementation below is per Gorden Shotwell’s recommendation on this thread. I hope Shiny for Python provides validate() equvalent.
Notifications
Transient notification
ui.notification_show() displays a message in the corner of the screen.
examples/action-feedback/transient-notification/app.py
from shiny import Inputs, Outputs, Session, App, reactive, render, req, ui
from time import sleep
app_ui = ui.page_fluid(
ui.input_action_button("goodnight", "Good night"),
)
def server(input: Inputs, output: Outputs, session: Session):
@reactive.effect
@reactive.event(input.goodnight)
def _():
ui.notification_show("So long")
sleep(1)
ui.notification_show("Farewell", type="message")
sleep(1)
ui.notification_show("Auf Wiedersehen", type="warning")
sleep(1)
ui.notification_show("Adieu", type="error")
app = App(app_ui, server)Shiny for Python website shows an example with asynchronous programming. I do not clearly understand what would be differences, but here is an asynchronous programming version implementation:
examples/action-feedback/transient-notification/app-async.py
from shiny import Inputs, Outputs, Session, App, reactive, render, req, ui
from asyncio import sleep
app_ui = ui.page_fluid(
ui.input_action_button("goodnight", "Good night"),
)
def server(input: Inputs, output: Outputs, session: Session):
@reactive.effect
@reactive.event(input.goodnight)
async def _():
ui.notification_show("So long")
await sleep(1)
ui.notification_show("Farewell", type="message")
await sleep(1)
ui.notification_show("Auf Wiedersehen", type="warning")
await sleep(1)
ui.notification_show("Adieu", type="error")
app = App(app_ui, server)Removing on completion
examples/action-feedback/removing-notification/app.py
from shiny import Inputs, Outputs, Session, App, reactive, render, req, ui
from shiny.types import FileInfo
import pandas as pd
app_ui = ui.page_fluid(
ui.input_file("file", "Add CSV file", accept=".csv"),
ui.output_table("table")
)
def server(input: Inputs, output: Outputs, session: Session):
@reactive.calc
def data():
id = ui.notification_show("Reading data...",
duration=None, close_button=False)
file: list[FileInfo] | None = input.file()
if file is None:
res = pd.DataFrame()
else:
res = pd.read_csv(file[0]["datapath"])
ui.notification_remove(id)
return res
@render.table
def table():
return data().head()
app = App(app_ui, server)I could not find Shiny for Python’s equivalent of Shiny for R’s on.exit() function. The app implementation above may be unreliable in a case when the reactive code chunk returns an error (e.g. when pd.read_csv() causes an error).
Progressive updates
examples/action-feedback/progressive-update/app.py
from shiny import Inputs, Outputs, Session, App, reactive, render, req, ui
from pydataset import data
from time import sleep
mtcars = data('mtcars')
app_ui = ui.page_fluid(
ui.output_table("table"),
)
def server(input: Inputs, output: Outputs, session: Session):
def notify(msg, id=None):
return ui.notification_show(msg, id=id, duration=None, close_button=False)
@reactive.calc
def data():
id = notify("Reading data...")
sleep(1)
notify("Reticulating splines...", id=id)
sleep(1)
notify("Herding llamas...", id=id)
sleep(1)
notify("Orthogonalizing matrices...", id=id)
sleep(1)
ui.notification_remove(id)
return mtcars
@render.table
def table():
return data().head()
app = App(app_ui, server)Progress bars
Shiny
examples/action-feedback/builtin-progress-bar/app.py
from shiny import App, ui, render, reactive
from time import sleep
import random
app_ui = ui.page_fluid(
ui.input_numeric("steps", "How many steps?", 10),
ui.input_action_button("go", "go"),
ui.output_text("result"),
)
def server(input, output, session):
@reactive.calc
@reactive.event(input.go)
def data():
with ui.Progress() as p:
p.set(message="Computing random number")
for i in range(1, input.steps()):
p.inc(1/input.steps())
sleep(0.5)
return random.uniform(0, 1)
@render.text
def result():
return round(data(), 2)
app = App(app_ui, server)The following code provides equally behaving progress bar:
with ui.Progress(min=1, max=input.steps()) as p:
p.set(message="Computing random number")
for i in range(1, input.steps()):
p.set(i)
sleep(0.5)- You can pass
minandmaxarguments when initializing the progress bar to set the starting point and end of the progress bar. If you do not provide, they set to bemin=0andmax=1by default. - Instead of
inc()that increment the progress bar byamountargument, you can useset()that update progress to bevalueargument.
Waiter
There is a python package py-waiter that provides python implementation of waiter package from John Coene. As of January 19, 2024, py-waiter only exists in github.
Let me further explore this package later, probably once the package is registered to PyPI.
Spinners
The same situation to waiter.
Confirming and undoing
Explicit confirmation
examples/action-feedback/explicit-confirmation/app.py
from shiny import App, ui, reactive
modal_confirm = ui.modal(
"Are you sure you want to continue?",
title="Delete files",
footer=ui.TagList(
ui.input_action_button("cancel", "Cancel"),
ui.input_action_button("ok", "Delete", class_="btn btn-danger"),
)
)
app_ui = ui.page_fluid(
ui.input_action_button("delete", "Delete all files?"),
)
def server(input, output, session):
@reactive.effect
@reactive.event(input.delete)
def delete():
ui.modal_show(modal_confirm)
@reactive.effect
@reactive.event(input.ok)
def ok():
ui.notification_show("Files deleted")
ui.modal_remove()
@reactive.effect
@reactive.event(input.cancel)
def cancel():
ui.modal_remove()
app = App(app_ui, server)For "cancel" button on modal dialog footer, because there is no associated tasks other than closing the modal dialog, you may want to consider using ui.modal_button() within the UI while removing server-side behavior for the button event:
examples/action-feedback/explicit-confirmation/app-modal-button.py
from shiny import App, ui, reactive
modal_confirm = ui.modal(
"Are you sure you want to continue?",
title="Delete files",
footer=ui.TagList(
ui.modal_button("Cancel"),
ui.input_action_button("ok", "Delete", class_="btn btn-danger"),
)
)
app_ui = ui.page_fluid(
ui.input_action_button("delete", "Delete all files?"),
)
def server(input, output, session):
@reactive.effect
@reactive.event(input.delete)
def delete():
ui.modal_show(modal_confirm)
@reactive.effect
@reactive.event(input.ok)
def ok():
ui.notification_show("Files deleted")
ui.modal_remove()
app = App(app_ui, server)Undoing an Action
I could not reproduct the app in python. Here are two topics that I do not know how to implemet in Shiny for Python. - Let observe run only once, which is equivalent to once = TRUE in observeEvent() in R. - Destory existing observer from outside the scope that the observer exists.