Basic UI

Inputs

Common structure

All input functions have the same first two argument:

  • id: the identifier used to connect the frontend with the backend: if your UI has an input with id="name", the server function will access it with input.name().
  • label: human-readable label for the control to appear on a web browser.

Free Text

examples/basic-ui/inputs/app.py
app_ui = ui.page_fluid(
    ui.input_text("name", "What's your name?"),
    ui.input_password("password", "What's your password?"),
    ui.input_text_area("story", "Tell me about yourself", rows=3),
)

Numeric inputs

examples/basic-ui/inputs/app.py
app_ui = ui.page_fluid(
    ui.input_numeric("num", "Number one", value=0, min=0, max=100),
    ui.input_slider("num2", "Number two", value=50, min=0, max=100),
    ui.input_slider("rng", "Range", value=(10, 20), min=0, max=100),
)
Note

For range slider, value argument can be a list (i.e. value=[10, 20]) instead of tuple, but in server function, input.rng() will be still return a tuple instead of a list.

Dates

examples/basic-ui/inputs/app.py
app_ui = ui.page_fluid(
    ui.input_date("dob", "When were you born?"),
    ui.input_date_range("holiday", "When do you want to go on vacation next?"),
)

Limited choices

Input controls for limited choices require choices argument to allow the user to choose from.

examples/basic-ui/inputs/app.py
state_names=[
    "Alabama", "Alaska", "Arizona", "Arkansas", "California", "Colorado", 
    "Connecticut", "Delaware", "Florida", "Georgia", "Hawaii", "Idaho", 
    "Illinois", "Indiana", "Iowa", "Kansas", "Kentucky", "Louisiana", 
    "Maine", "Maryland", "Massachusetts", "Michigan", "Minnesota", 
    "Mississippi", "Missouri", "Montana", "Nebraska", "Nevada", 
    "New Hampshire", "New Jersey", "New Mexico", "New York", 
    "North Carolina", "North Dakota", "Ohio", "Oklahoma", "Oregon", 
    "Pennsylvania", "Rhode Island", "South Carolina", "South Dakota", 
    "Tennessee", "Texas", "Utah", "Vermont", "Virginia", "Washington", 
    "West Virginia", "Wisconsin", "Wyoming"]

animals = ["dog", "cat", "mouse", "bird", "other", "I hate animals"]
examples/basic-ui/inputs/app.py
app_ui = ui.page_fluid(
    ui.input_select("state", "What's your favourite states?", choices=state_names,
    ui.input_radio_buttons("animal", "What's your favourite animals?", choices=animals),
)

choices can be a dictionary instead of a list. In such cases, value is displayed on a screen, but key is retrieved when the input is accessed from a server function.

For example to show icon on screen, let us use faicons package, which provides an interface to Font-Awesome for use in Shiny for Python.

examples/basic-ui/inputs/app.py
from faicons import icon_svg

app_ui = ui.page_fluid(
    ui.input_radio_buttons("rb", "Choose one:", choices={
        "angry": icon_svg("face-angry"), "happy": icon_svg("face-smile"), "sad": icon_svg("face-sad-tear")}),
)
Note

On app, icons appear on each radio button items. However, when you call input.rb() in a server, it returns key values "angry", "happy", or "sad" based on which item was chosen by a user.

ui.input_select() and ui.input_selectize() allow a user to select multiple items when multiple=True.

examples/basic-ui/inputs/app.py
app_ui = ui.page_fluid(
    ui.input_selectize("states", "What's your favourite states?", choices=state_names, multiple=True),
)

Radio button allows only a single item selection. Mutliple selection can be done through alternative control: ui.input_checkbox_group().

examples/basic-ui/inputs/app.py
app_ui = ui.page_fluid(
    ui.input_checkbox_group("animals", "What animals do you like?", choices=animals),
)

Single checkbox ui.input_checkbox() is for Yes/No questions.

examples/basic-ui/inputs/app.py
app_ui = ui.page_fluid(
    ui.input_checkbox("cleanup", "Clean up?", value=True),
    ui.input_checkbox("shutdown", "Shutdown?"),
)

File uploads

examples/basic-ui/inputs/app.py
app_ui = ui.page_fluid(
    ui.input_file("upload", None),
)

Action buttons

examples/basic-ui/inputs/app.py
app_ui = ui.page_fluid(
    ui.input_action_button("click", "Click me!"),
    ui.input_action_button("drink", "Drink me!", icon=icon_svg("martini-glass-citrus")),
)

You can customise the appearance using the class_ argument.

examples/basic-ui/inputs/app.py
app_ui = ui.page_fluid(
    ui.input_action_button("clickdanger", "Click me!", class_="btn-danger"),
    ui.input_action_button("drinklarge", "Drink me!", class_="btn-lg btn-success"),
    ui.br(),
    ui.input_action_button("eat", "Eat me!", class_="w-100"),
)
Note

ui.br() is to create <br> HTML tag which is a line break syntax.

Note

In Mastering Shiny book, there is also example with class name btn-block to create full-width block buttons. It does not work in shiny package in python with version 0.6.1.1. I do not know the reason, but I guess that it may be because Shiny for Python is based on Bootstrap v5 that implement block buttons not through class but through layout. As a stop-gap solution for this example, I used class name w-100 from sizing classes.

Exercises

  1. Label text boxes using a placeholder that appears inside the text entry area.
solutions/basic-ui/inputs/placeholder/app.py
from shiny import App, ui

app_ui = ui.page_fluid(
    ui.input_text("name", None, placeholder="Your name"),
)

def server(input, output, session):
    ...

app = App(app_ui, server)
  1. Create a date slider.
solutions/basic-ui/inputs/dateslider/app.py
from shiny import App, ui
import datetime

app_ui = ui.page_fluid(
    ui.input_slider("date", "When should we deliver?",
                    min=datetime.date(2020, 9, 16),
                    max=datetime.date(2020, 9, 23),
                    value=datetime.date(2020, 9, 17)),
)

def server(input, output, session):
    ...

app = App(app_ui, server)
  1. Create a slider input to select values between 0 and 100 where the interval between each selectable value on the slider is 5. Then, add animation to the input widget so when the user presses play the input widget scrolls through the range automatically.
solutions/basic-ui/inputs/animateslider/app.py
from shiny import App, ui

app_ui = ui.page_fluid(
    ui.input_slider("ani", None, min=0, max=100, value=0,
                    step=5, animate=True, ticks=True),
)

def server(input, output, session):
    ...

app = App(app_ui, server)
Note

ticks=True is not an essential argument for this exercise, but it guides a user to click a right position when selecting a number.

  1. Create sub-headings that break the list up into pieces.
solutions/basic-ui/inputs/subheading/app.py
from shiny import App, ui

grouped = {
    "Group A": {"A1":"Item A1", "A2":"Item A2"},
    "Group B": {"B1":"Item B1", "B2":"Item B2", "B3":"Item B3"},
    "Group C": {"C1":"Item C1", "C2":"Item C2"}
}

app_ui = ui.page_fluid(
    ui.input_select("long", "Choose:", choices=grouped),
)

def server(input, output, session):
    ...

app = App(app_ui, server)
Note

A dictionary of dictionaries used as choices argument. It would mean that server will use keys (e.g. "A1") as input values instead of appeared string (e.g. "Item A1"), so keys would need to be unique not only within a subgroup but also across subgroups.

Outputs

Text

examples/basic-ui/outputs/text/app.py
from shiny import App, ui, render
import pandas as pd

app_ui = ui.page_fluid(
    ui.output_text("text"),
    ui.output_text_verbatim("code"),
)

def server(input, output, session):
    @output
    @render.text
    def text():
        return "Hello friend!"
    
    @output
    @render.text
    def code():
        x = pd.Series(range(1, 11))
        return x.describe()

app = App(app_ui, server)
Note

In R, text output uses render function renderText() and verbatim output uses render function renderPrint(). In Python, both text and verbatim outputs use the same render function render.text().

Tables

examples/basic-ui/outputs/tables/app.py
from shiny import App, ui, render
from pydataset import data

mtcars = data("mtcars")

app_ui = ui.page_fluid(
    ui.output_table("static"),
    ui.output_data_frame("dynamic"),
)

def server(input, output, session):
    @output
    @render.table
    def static():
        return mtcars
    
    @output
    @render.data_frame
    def dynamic():
        return render.DataTable(mtcars, filters=True)

app = App(app_ui, server)
Note

While you can return just mtcars in dynamic() for default interactive table, to enable filters, either render.DataGrid() or render.DataTable() needs to be returned with filter=True.

Note

The dynamic table is somewhat different from R version. There may be more ways to render tables by using widgets.

Plots

examples/basic-ui/outputs/plots/app.py
from shiny import App, ui, render
from matplotlib import pyplot as plt

app_ui = ui.page_fluid(
    ui.output_plot("plot", width="400px")
)

def server(input, output, session):
    @output
    @render.plot
    def plot():
        return plt.scatter([1, 2, 3, 4, 5], [1, 2, 3, 4, 5])

app = App(app_ui, server)

Exercises

  1. Text outputs and verbatim outputs
from shiny import App, ui, render
from pydataset import data
from scipy.stats import ttest_ind
import statsmodels.formula.api as smf

mtcars = data("mtcars")

app_ui = ui.page_fluid(
    ui.output_text_verbatim("summary"),
    ui.output_text("greeting"),
    ui.output_text_verbatim("ttest"),
    ui.output_text_verbatim("regression"),
)

def server(input, output, session):
    @output
    @render.text
    def summary():
        return mtcars.describe()
    
    @output
    @render.text
    def greeting():
        return "Good morning!"
    
    @output
    @render.text
    def ttest():
        return ttest_ind([1, 2, 3, 4, 5], [2, 3, 4, 5, 6])
    
    @output
    @render.text
    def regression():
        reg = smf.ols("mpg ~ wt", data=mtcars).fit()
        return reg.summary()

app = App(app_ui, server)
Note

Shiny for Python use render.text() for both text output and verbatim output.

Note

mtcars.describe() returns pandas data frame. When render.text() is used, the output will look like a result of print(mtcars.describe()).

  1. Plot by seeting height to 300px, witdh to 700px, and “alt” text so that a visually impaired user can tell that its a scatterplot of five random numbers.
solutions/basic-ui/outputs/plots/app.py
from shiny import App, ui, render
from matplotlib import pyplot as plt
import numpy as np

app_ui = ui.page_fluid(
    ui.output_plot("plot", width="700px", height="300px")
)

def server(input, output, session):
    @output
    @render.plot(alt="A scatterplot of five random numbers")
    def plot():
        return plt.scatter(np.random.uniform(size=5), np.random.uniform(size=5))
    
app = App(app_ui, server)
  1. Dynamic data table without search, search, ordering, and filtering commands.
solutions/basic-ui/outputs/tables/app.py
from shiny import App, ui, render
from pydataset import data

mtcars = data("mtcars")

app_ui = ui.page_fluid(
    ui.output_data_frame("dynamic"),
)

def server(input, output, session):
    @output
    @render.data_frame
    def dynamic():
        return render.DataTable(mtcars, filters=False, height='240px')

app = App(app_ui, server)
Warning

The rendered table still provides ordering commands. I do not know how to disable the ordering command with built-in render functions in shiny python package.

  1. Use React Table.
Caution

I could not find a way to render React Table or any other interactive tables that are not included in shiny package.

I attemped to use ipywidgets via shinywidgets, but my attempt was not quite successful and satisfiable.

It looks there have been related discussions on github.

I hope to come back this exercise once I find a solution.