Try Shiny Express

Shiny makes it easy to create rich, interactive experiences in pure Python with a fully reactive framework. No JavaScript required!

Shiny Express

Here’s the Shiny application we’ll be creating. Give it a try! See how the different penguin distributions compare to one another.

#| '!! 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 palmerpenguins import load_penguins
from plotnine import aes, geom_histogram, ggplot, theme_minimal
from shiny.express import input, render, ui

dat = load_penguins()
species = dat["species"].unique().tolist()

ui.input_radio_buttons("species", "Species", species, inline=True)


@render.plot
def plot():
    sel = dat[dat["species"] == input.species()]
    return (
        ggplot(aes(x="bill_length_mm"))
        + geom_histogram(dat, fill="#C2C2C4", binwidth=1)
        + geom_histogram(sel, fill="#447099", binwidth=1)
        + theme_minimal()
    )

Goal

Let’s create a simple shiny application that uses the palmerpenguins dataset and visualizes it using plotnine.

We’ll use shiny to give us interactive radio buttons to change what species of the bill_length_mm variable we will want to highlight.

Prototype

We’ll be using palmerpenguins for the dataset, and plotnine to visualize the palmer penguin’s bill_length_mm column as a histogram.

from palmerpenguins import load_penguins

dat = load_penguins()
dat.head()
species island bill_length_mm bill_depth_mm flipper_length_mm body_mass_g sex year
0 Adelie Torgersen 39.1 18.7 181.0 3750.0 male 2007
1 Adelie Torgersen 39.5 17.4 186.0 3800.0 female 2007
2 Adelie Torgersen 40.3 18.0 195.0 3250.0 female 2007
3 Adelie Torgersen NaN NaN NaN NaN NaN 2007
4 Adelie Torgersen 36.7 19.3 193.0 3450.0 female 2007

Here’s the base code we’ll be starting with. It creates 2 histograms layerd on top of one another. The base layer is the distribution of all the penguin species. and the blue layer on top will the distribution of the selected penguin species.

from plotnine import aes, geom_histogram, ggplot, theme_minimal

species = "Adelie" # selected species
sel = dat.loc[dat.species == species] # selected data

(
    ggplot(aes(x="bill_length_mm"))
    + geom_histogram(dat, fill="#C2C2C4", binwidth=1)
    + geom_histogram(sel, fill="#447099", binwidth=1)
    + theme_minimal()
)

Learn more about plotting in plotnine

To learn more about how the plotting code works in plotnine, take a look at our plotnine lab.

Wouldn’t it be nice if we could have something to toggle between one of the 3 penguin species instead of having us manually change the species variable and re-run our code?

Now we’re ready for a Shiny dashboard! All we need is some mechanism to list and provide the options for the user to pass into the species variable. Then pandas can filter the selected layer data, and plotnine can render the new data layer.

Shiny Express

Shiny Express is designed to make it significantly easier to get started with Shiny, and to write simple apps with a minimum of boilerplate.

Wherever you write your UI or output code, is where it will render in the application. In this sense Shiny Express is similar to Streamlit. If you’re looking for syntax where the UI and outputs are separated, similar to Dash, then take a look at the Shiny Core Lab.

User interface

Before we get all components reacting to one another, let’s start with mapping out how our application will look.

Shiny comes with all the input and output components you will need. For now we’ll focus on the input_radio_buttons component.

We will pass input_radio_buttons() 3 arguments:

  1. id: this is what shiny uses to reference the value selected by the input (more on this later).
  2. label: the text the user will see by the component.
  3. choices: the options our radio button component will display to the user and value it will use in code.
#| '!! 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
from shiny.express import ui

ui.input_radio_buttons(
    id="species",
    label="Species",
    choices=["Adelie", "Gentoo", "Chinstrap"],
)

Run your shiny application

In Positron or VSCode, assuming you have the VSCode Shiny Extension installed, create a file named app.py, copy + paste the code above and hit the play button to run your application.

Or you can go to https://shinylive.io/py and copy + paste the code there and run your application.

See our Shiny documentation to learn more about running shiny applications.

Improve UI

Let’s have the buttons run horizontally to take up less space. We can use the inline=True to change the orientation of the radio buttons.

#| '!! 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
from shiny.express import ui

ui.input_radio_buttons(
    id="species",
    label="Species",
    choices=["Adelie", "Gentoo", "Chinstrap"],
    inline=True,
)

Output component

Now let’s add all that data and plotting code from earlier into our application.

If we just dump in our code, the application errors because it does not know what to do with the figure that’s trying to be printed.

from shiny.express import ui

from palmerpenguins import load_penguins
from plotnine import aes, geom_histogram, ggplot, theme_minimal

ui.input_radio_buttons(
    id="species",
    label="Species",
    choices=["Adelie", "Gentoo", "Chinstrap"],
    inline=True,
)

dat = load_penguins()
species = "Adelie"
sel = dat.loc[dat.species == species]

# this will cause a TypeError: Invalid tag item type
(
    ggplot(aes(x="bill_length_mm"))
    + geom_histogram(dat, fill="#C2C2C4", binwidth=1)
    + geom_histogram(sel, fill="#447099", binwidth=1)
    + theme_minimal()
)

In shiny, each output needs to be wrapped in it’s own function, and the corresponding output decorator applied to the function so Shiny knows how to render the output.

We now need to use one of the built-in Shiny output components, specifically, one that can render a plotnine figure (which is based on matplotlib), plot output component.

For example, we want to return a plot, so we will need to wrap our plotnine code, and decorate it with the @render.plot decorator.

#| '!! 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
from shiny.express import ui, render

from palmerpenguins import load_penguins
from plotnine import aes, geom_histogram, ggplot, theme_minimal

ui.input_radio_buttons(
    id="species",
    label="Species",
    choices=["Adelie", "Gentoo", "Chinstrap"],
    inline=True,
)

dat = load_penguins()
species = "Adelie"
sel = dat.loc[dat.species == species]

@render.plot 
def plot(): 
    return ( 
        ggplot(aes(x="bill_length_mm"))
        + geom_histogram(dat, fill="#C2C2C4", binwidth=1)
        + geom_histogram(sel, fill="#447099", binwidth=1)
        + theme_minimal()
    )
Return your values

Don’t forget to return the object you want displayed in the function! Otherwise the output will not render.

Make it reactive

Great! We have our input component, we have our output component, all we need to do now is connect the value selected by the input component and have the output react to the selected input.

First, when we are working with linking input and output values, they must all exist in the same “reactive context”, i.e., be in a function that is decorated by shiny, in our case all our code that needs to react needs to be in the function that is decorated by @render.plot.

Reactivity

Reactivity is actually what makes Shiny powerful and unique. You can learn more about reactivity in our documentation.

#| '!! 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
from shiny.express import ui, render

from palmerpenguins import load_penguins
from plotnine import aes, geom_histogram, ggplot, theme_minimal

ui.input_radio_buttons(
    id="species",
    label="Species",
    choices=["Adelie", "Gentoo", "Chinstrap"],
    inline=True,
)

dat = load_penguins()

@render.plot
def plot():
    species = "Adelie" 
    sel = dat.loc[dat.species == species] 

    return (
        ggplot(aes(x="bill_length_mm"))
        + geom_histogram(dat, fill="#C2C2C4", binwidth=1)
        + geom_histogram(sel, fill="#447099", binwidth=1)
        + theme_minimal()
    )

Now we’re ready to link the input and output components together. We now need to import the input object from shiny. The input object keep track of all the input components in the application and allows you to access the input value by the id we used when we created the input component.

In our example, our input_radio_buttons() input component was given an id="species", so we can access the value in the radio button by using input.species(). Here we have to add round parenthesis to the value, because we’re trying to get the actual value stored in the species input.

Don’t forget the round parenthesis ()!

Don’t forget to add a set of round parenthesis, () when you are trying to get the value of a component. You need to “call” it for Shiny to compute its value. This is how we work with reactivity in shiny, and this prevents us from writing callbacks in our application.

Instead of hard-coding species = "Adelie", we can replace it with species = input.species(), and it will return the value from our radio button input component.

#| '!! 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
from shiny.express import ui, render, input 

from palmerpenguins import load_penguins
from plotnine import aes, geom_histogram, ggplot, theme_minimal

ui.input_radio_buttons(
    id="species",
    label="Species",
    choices=["Adelie", "Gentoo", "Chinstrap"],
    inline=True,
)

dat = load_penguins()

@render.plot
def plot():
    species = input.species() 
    sel = dat.loc[dat.species == species]

    return (
        ggplot(aes(x="bill_length_mm"))
        + geom_histogram(dat, fill="#C2C2C4", binwidth=1)
        + geom_histogram(sel, fill="#447099", binwidth=1)
        + theme_minimal()
    )

Our final application

After tidying up the code a bit, here’s our final Shiny application!

If you want you can tinker with the code and run the application right in the browser, or copy + paste the code and run it in your IDE.

#| '!! 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 palmerpenguins import load_penguins
from plotnine import aes, geom_histogram, ggplot, theme_minimal
from shiny.express import input, render, ui

dat = load_penguins()
species = dat["species"].unique().tolist()

ui.input_radio_buttons("species", "Species", species, inline=True)


@render.plot
def plot():
    sel = dat[dat["species"] == input.species()]
    return (
        ggplot(aes(x="bill_length_mm"))
        + geom_histogram(dat, fill="#C2C2C4", binwidth=1)
        + geom_histogram(sel, fill="#447099", binwidth=1)
        + theme_minimal()
    )

Learn more