Learn Shiny for Python with a Puppy Traits Dashboard

Author:Murphy  |  View: 29826  |  Time: 2025-03-22 21:44:19

Exploring Shiny for Python With A Puppy Traits Web Application

Image by author

Shiny has been revered for over a decade within the R-ecosystem, and recently all its goodness was introduced to Python as well. Shiny is a web application framework that can be used to create interactive web applications that can run code in the backend.

I have been an avid user of R Shiny for years, so I was naturally excited when it was introduced to Python as well. In this article, I will walk through the steps I took to create my "Who is the Goodest Doggy" application , from foundations to styling. Let's get started!


Code and data availability

Code for recreating everything in this article can be found in my GitHub repo.

Data: This article uses synthetic data with fake dog trait ratings generated by me. The synthetic data is available at the linked GitHub page and was inspired by the data I found on TidyTuesday, courtesy of KKakey and originally sourced from true sightings at American Kennel Club.

Data generation process: To generate the data, I took the unique dog and trait combinations and used numpy library to generate a random rating between 2–5 for each breed and characteristic. The full code to generate this dataset can be found at the linked repo within the data folder.


Setting up the Python environment

  • Install Shiny for Python extension (For VSCode)
  • Create a virtual environment for your project (good practice)
  • Install the required packages by either running individual commands like "pip install shiny" or installing all packages by running "pip install -r requirements.txt".

Base components of Shiny

This foundation command is my favorite because the Posit team has made it very easy to get up and running with base Shiny templates.

Shiny create --help
Image by author

This command opens up the options for the various Shiny templates. Every time I check there is a new template on this list as it is being developed very fast. The image above shows what appears when the command is run. I will pick the basic-app template and run the command shiny create -t basic-app.

This command created the app template with a simple app.py file that generated the Shiny dashboard below when run. In this app, the user can choose a number on the scrollbar, and the app updates the calculation and written text underneath it.

Image by author

Let's take a look at the app.py file contents:

from shiny import App, render, ui # Import required packages

app_ui = ui.page_fluid(
    ui.panel_title("Hello Shiny!"), # Create the panel for title
    ui.input_slider("n", "N", 0, 100, 20), # Create the slider
    ui.output_text_verbatim("txt"), # Create space for text on the UI
)

def server(input, output, session):
    @render.text # Decorator to define output format
    def txt(): # The function that will create the text to send over to the ui
        return f"n*2 is {input.n() * 2}" # The text calculation

app = App(app_ui, server)

Similar to Shiny for R, there is a ui and server component within the app.py file. The ui contains the frontend of the application and the server logic handles the backend. I will update this template for exploring puppy traits dataset.


Personalizing the application

Adding dataframe and plot outputs

After choosing the base template, I will update the app.py file for my data and analysis needs. To output a dataframe and a plot in my application, I can update the ui and server logic to read and display the dataframe as a pandas object and call a plotting function on the server side.

from shiny import App, render, ui
import pandas as pd # Import pandas for working with data frames
from pathlib import Path
from trait_rating_plot import create_trait_rating_plot # Import plot function

# Read the dataframe
df = pd.read_csv(Path(__file__).parent / "dog_traits.csv", na_values = "NA")

# UI logic
app_ui = ui.page_fillable(
    ui.output_data_frame("dog_df"), # Create space for plot output
    ui.output_plot("breed_plot") # Create space for dataframe output
)

# Server logic
def server(input, output, session):
    @render.data_frame # Decorator to define output structure for data frame
    def dog_df():
        return df

    @render.plot # Decorator to define output structure for plot
    def breed_plot():
        fig = create_trait_rating_plot(df, "Bulldogs") # Call external function to generate the plot for Bulldogs
        return fig

app = App(app_ui, server)

The above code creates the Shiny output shown below. Since it is neither pretty nor dynamic yet, there is quite some work to be done. For now this is a static output. The dataframe output shows the entire dataset. The plot output is hardcoded for "Bulldogs". Their placement it simply one followed by another as the application is not yet structured with its layout.

Image by author

Adding dynamic components: Sidebar layout and filters

Sidebar

This is one of the most common layouts I have seen on Shiny applications. In the above code, I will encompass the ui components into a ui.page_sidebar() function. This function has two parts:

  • The first part sits inside ui.sidebar() function. All ui components that need to show up in the side panel are coded within this function.
  • The second part is everything else, which sits in the main panel area.

Filters inside the sidebar panel

Now that I have my sidebar, I will add a user input dropdown filter for the dog breed using the ui.input_select() function so that my plot is not hardcoded for "Bulldogs", and can be updated for any dog.

I will also add a multi-selection filter for dog characteristics that the user is interested in. This will only update the dataframe to show the characteristics selected in order to compare them amongst dogs.

Updated ui logic:

# Read the dataframe
df = pd.read_csv(Path(__file__).parent / "dog_traits.csv", na_values = "NA")
breeds = df.breed.unique().tolist()
traits = df.trait.unique().tolist()

app_ui = ui.page_fillable(
    ui.page_sidebar( # Sidebar layout
        ui.sidebar( # First part - sidebar panel
            ui.input_select("inputbreed", # Single input filter for dog breed
                    label = "Select breed", 
                    choices = breeds, 
                    selected="Bulldogs"),
            ui.input_selectize(id = "inputtrait", # Multi input filter for characteristics
                    label= "Select traits", 
                    choices = traits, 
                    multiple=True, 
                    selected="Adaptability Level"),
        ),
        ui.output_data_frame("dog_df"), # Main panel
        ui.output_plot("breed_plot") # Main panel
    ),
)

On the server side:

  • I will update the plot output function to take the user input breed instead of default "Bulldogs". This makes the plot dynamically react to the dog breed input.
  • I will update the dataframe output to add the characteristics filtering logic based on user selection. This makes the dataframe update dynamically for the selected characteristics by the user.
def server(input, output, session):
    @render.data_frame
    def dog_df(): # Filter and sort based on user characteristics selection
        filtered_df = df[(df['trait'].isin(input.inputtrait()))]
        return filtered_df.sort_values(by=["trait", "rating"], ascending=[True, False])

    @render.plot
    def breed_plot(): # filter and sort based on user breed selection
        df_updated = df.copy()
        fig = create_trait_rating_plot(df_updated, input.inputbreed())
        return fig

As in the image below, there is now a sidebar layout with the filters sitting in the side panel and everything else in the main panel.

Image by author

Updating multiple outputs with "reactive calculations"

So far, I added two filters both of which impact only one of the outputs. I also want to add slider inputs for minimum and maximum ratings that the user wants to see reflected in both the data frame and the plot outputs. In this scenario, I will create slider inputs on the ui side for the user to select rating limits.

ui.input_slider(id = "ratingmin", # Slider input for minimum rating
                label="Minimum rating", 
                min=1, max=5, value=1),
ui.input_slider(id = "ratingmax", # Slider input for maximum rating
                label="Maximum rating", 
                min=1, max=5, value=5),

I will then create a new table output on the server side that filters the whole table based on the rating limits selected. This is called a "reactive calculation" and hence it sits under the decorator reactive.Calc.

I will use this new reactive calculation output as an input to the other two server functions for my dataframe and plot, so both of them get impacted by the slider inputs. The output of a reactive calculation can be accessed in other server functions using reactive brackets.

def server(input, output, session):
    @reactive.Calc # New reactive calculation for filtering data by rating
    def filtered_ratings(): 
        filtered_rating = df[(df['rating'] >= input.ratingmin()) &
                     (df['rating'] <= input.ratingmax())]
        return filtered_rating.sort_values(by=["trait", "rating"], ascending=[True, False])

    @render.data_frame # updated to use "filtered_ratings()" instead of original df.
    def dog_df():
        filtered_df = filtered_ratings()[(filtered_ratings()['trait'].isin(input.inputtrait()))]
        return filtered_df.sort_values(by=["trait", "rating"], ascending=[True, False])

    @render.plot # updated to use "filtered_ratings()" instead of original df.
    def breed_plot():
        fig = create_trait_rating_plot(filtered_ratings(), input.inputbreed())
        return fig

Updated application output:

Image by author

Adding "Apply" settings button

The concern with having multiple filters is that every time one of them is updated, the application gets updated immediately instead of waiting for the user to update every filter they need to change before refreshing the application. I will add an Apply button that will allow the outputs to get updated based on the selected filters once everything has been inputted and the button is clicked. The button is added within the ui logic as below:

  ui.input_action_button("apply", # id of the button to access on the server side
                        "Apply settings", # Externally visible label
                        class_="btn-secondary") # The color

On the server side, I will update the dataframe and plot output functions to become "reactive events", so that they will react only when "Apply" is pressed.

@render.data_frame
@reactive.event(input.apply, ignore_none=False)
def dog_df():
    filtered_df = filtered_ratings()[(filtered_ratings()['trait'].isin(input.inputtrait()))]
    return filtered_df.sort_values(by=["trait", "rating"], ascending=[True, False])

@render.plot
@reactive.event(input.apply, ignore_none=False)
def breed_plot():
    fig = create_trait_rating_plot(filtered_ratings(), input.inputbreed())
    return fig

When the application is run, there should be an additional "Apply" button along other filters.

Now, I will finally work to make the app look more presentable!


UI layouts

From here, I will work to update only the ui components that will reflect on the frontend.

Adding a boundary around the plots and updating to column layout

I will now add a boundary around both my dataframe and plot by encompassing each of those elements within ui.card() function. This function also allows adding a heading by using ui.card_header() within the ui.card() function.

I will then encompass both of these ui elements within the ui.layout_columns() function so that I can assign them designated column space that I want each ui component to occupy. There are total 12 units I can assign to the components within a column layout. I will assign the dataframe and plot a space of 5 and 7 units respectively.

ui.layout_columns( # Encompass all ui components within the main panel into column layout
    ui.card( # Encompass dataframe space within ui.card to create boundary
    ui.card_header("Select the traits to update this plot"), # Card title
    ui.output_data_frame("dog_df"),
    ),
    ui.card( # Encompass plot space within ui.card to create boundary
        ui.card_header("Select the breed to update this plot"), # Card title
        ui.output_plot("breed_plot")
    ),
    gap = "2rem", # Column layout function input for gap between columns
    col_widths={"sm": (5, 7)}, # Size of the columns for each component
    height = "400px" # Height of the columns
    )

Updated application output:

Image by author

Looks so much cleaner already.


Images, texts and panels

Next, since this is a puppy dashboard, I want to add an irresistible puppy image at the top of my dashboard side by side with the heading. For these two to be beside each other at the top of the dashboard, I will add a row above my plot and dataframe section. Within this row I will create another column layout and use the following functions.

  • ui.tags.img(): I will use this function to add an image within this layout.
  • ui.tags.h1(): I will use this function to add the dashboard heading. There are a lot of other capabilities of ui.tags that are available in the Shiny documentation.
  • ui.markdown(): I will add a data source call out using simple text with this function.
  • ui.panel_absolute(): I will add the dashboard heading and markdown text within a panel using this function that stays visible while scrolling the dashboard.
  • ui.panel_conditional(): I will create a conditional panel for my ratings filter, so they are visible only when the checkbox to "Set limits for ratings" is selected.
# Puppy image url
dogimg_url = "https://images.unsplash.com/photo-1444212477490-ca407925329e?q=80&w=1856&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"

# Row layout
ui.row(
  # Multiple columns within each row
    # First column for the image using ui.tags.img
    ui.column(6, ui.tags.img(src=dogimg_url, height="90%", width="100%")),
    # Second column for the dashboard heading and data source call out
    ui.column(5,
        ui.panel_absolute(  
          ui.panel_well(
            ui.tags.h1("Who is the goodest doggy?!?"),
            ui.markdown("Data: Synthetic dog trait ratings inspired by TidyTuesday dataset courtesy of [KKakey](https://github.com/kkakey/dog_traits_AKC/blob/main/README.md) sourced from the [American Kennel Club](https://www.akc.org/)."),
            ui.markdown("Photo by [Anoir Chafik](https://unsplash.com/@anoirchafik?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash) on [Unsplash](https://unsplash.com/photos/selective-focus-photography-of-three-brown-puppies-2_3c4dIFYFU?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash)")
    ),
          width="450px",  
          right="75px",  
          draggable=False,  
         )
    )
)

Code for the conditional panel:

ui.input_checkbox("show", "Set limits for ratings", False), # Create a checkbox for conditional input
    ui.panel_conditional( # Only show the components within this function when condition is true
        "input.show",  # Condition that the checkbox is selected
        ui.input_slider(id = "ratingmin", label="Minimum rating", min=1, max=5, value=1),
        ui.input_slider(id = "ratingmax", label="Maximum rating", min=1, max=5, value=5),
     )

Updated application:

Image by author

Themes

Color schemes and themes are so important, because I think they define the personality and branding of the dashboard. I will update the background of my sidebar panel as well as the theme of the entire dashboard.

  • Sidebar panel color: Within the ui.sidebar() function I will use "bg" input to provide the background color. I will also provide it the input "open" so that the sidebar is open by default, and the user has an option to collapse it and reopen it.
  • I will use shinyswatch package to import themes for the dashboard. There are multiple themes available through this package but for this dashboard I loved theme.minty().

Updated ui code:

from shinyswatch import theme # import the package for various prebuilt themes

....

app_ui = ui.page_fillable(
    theme.minty(), # Call minty() theme to update the page
       ...
       ui.sidebar( # Sidebar panel
          ...,
          bg="#f6e7e8", open="open" # Panel variable inputs
        )
)

The final application

The application is now ready with all the elements that I added step by step. Here is the final output and the end to end code to reproduce it is in the GitHub repo.

Image by author

Why is now a great time to get started with Shiny for Python

I think a few reasons make right now the perfect time to enter this space.

  • Shiny for Python is just getting started. If R Shiny is any indicator, there is a lot more coming and Posit has been moving very quickly to incorporate python in their ecosystem.
  • There are new applications coming up like Shiny Express that are making Shiny for Python even more accessible for quick prototypes.
  • If you come from a Shiny for R background, core Shiny for Python is a great step into the world of Python.
  • If you come from a Streamlit for Python background, Shiny express provides that perfect balance between Streamlit's agility for prototyping and Shiny's efficiency.

I hope you found this article encouraging to go learn and build Shiny for Python web applications. Keep an eye out for the next article in my Shiny series on Python Shiny Express, the newest kid on the block.


Code for recreating everything in this article can be found at https://github.com/deepshamenghani/shinypython_meetup/.


If you'd like, find me on Linkedin.

Tags: Data Science Data Visualization Hands On Tutorials Python Shiny

Comment