GitHub
LinkedIn
Twitter
YouTube
RSS

Hello Shiny Python

Author: Jamie Owen

Published: August 23, 2022

We would posit (see what we did there) that R-{shiny} has been a boon for data science practitioners using the R language over the last decade. We know that in our Python work, we have certainly been clamouring for something of the same ilk. And whilst there are other frameworks that we also like, streamlit and dash to name a couple, neither of them has filled us with the same excitement and confidence that shiny did in R to build both simple and complex bespoke web applications. With RStudio Posit conf in action the big news from July 27th was the alpha release of Py-{shiny} which was a source of great interest for us, so we couldn’t resist installing and starting to build.

If you are familiar with R-shiny already, then much of the py-shiny package will feel familiar to you (albeit with a couple of things having been renamed). However we will approach the rest of this post assuming that a reader does not have that prior experience and take you through building a simple shiny application to display plots on subsets of a dataset.


Do you use RStudio Pro? If so, checkout out our managed RStudio services


Installing

As it is released on pypi.org, installing shiny is trivial as with any other python package. We can set up a virtual environment with shiny and the other dependencies we will use as follows:

virtualenv .venv
source .venv/bin/activate
pip install shiny plotnine

Creating an application

An application is composed of two parts, the user interface (UI) front end that the end user will use to navigate your application and a server function which defines the logic of your application.

The UI functions in the shiny library just generate html, css and JavaScript code which will be shipped to the browser and rendered. A basic page might be created with

from shiny import ui

frontend = ui.page_fluid(
  "Hello Shiny Python"
)

If you were to print this object out to the console you will see the html tags that are being generated

frontend
## <html>
##   <head></head>
##   <body>
##     <div class="container-fluid">Hello Shiny Python</div>
##   </body>
## </html>

digging into the source a little deeper we can find that it will also include bootstrap and jquery as dependencies. These packages will provide the backbone for the default visuals of html elements and the communication between client side browser and back end logic.

To run an app we also need a server function, the server function has 3 parameters,

  • input: An object that contains the bindings to values being set by the client side JavaScript
  • output: An object that will allow us to send responses back to our front end
  • session: An object that contains data and methods related to a specific user session

To display our front end we could define a simple server function

# of course this server won't do anything useful, but is
# enough to run an app to display our current UI
def server(input, output, session):
  return True

And finally create a shiny.App object to link the two things together

from shiny import App

app = App(frontend, server)

The full source, which, for this blog post, we’ve called “hello.py”, for this trivial example would be

# hello.py
from shiny import ui, App

frontend = ui.page_fluid(
  "Hello Shiny Python"
)

def server(input, output, session):
  return True

app = App(frontend, server)

Running my application

The {shiny} python package also has a command line tool to run the application.

shiny run --port 5000 hello.py

would start the app. Navigate to “localhost:5000” to see your app.

Something a little more interesting

We will build a simple web application that will let us dynamically update a plot of prices of different diamonds, depending on the user choice of colours.

{shiny} python provides a host of widgets that can be used to take input from a user, which will send that data to the back end server. It also provides a set of output containers in which to render results received from the server. A simple user interface for this might be defined as follows:

from shiny import ui
import pandas as pd
from plotnine.data import diamonds

# function that creates our UI based on the data
# we give it
def create_ui(data: pd.DataFrame):
  # calculate the set of unique choices that could be made
  choices = data['color'].unique()
  # create our ui object
  app_ui = ui.page_fluid(
    # row and column here are functions
    # to aid laying out our page in an organised fashion
    ui.row(
      ui.column(2, offset=1,*[
        # an input widget that allows us to select multiple values
        # from the set of choices
        ui.input_selectize(
          "select", "Color",
          choices=list(choices),
          multiple=True
        )]
      ),
      ui.column(1),
      ui.column(6,
        # an output container in which to render a plot
        ui.output_plot("out")
      )
    )
  )
  return app_ui

frontend = create_ui(diamonds)

The first arguments of both the input widget and output container are a unique id. These ids will be used on the server side to identify the inputs and outputs that we want to work with.

Our input_selectize("select", ...) function, will respond to the user action taken in the browser and send the chosen values as an input to the server function. This value is accessible based on its unique id “select” as input.select(). Similarly the output_plot("out") container will allow us to draw a chart into that position in the application using the @output(id="out") decorator referring to the id “out”. The output decorator will be accompanied by a renderer which will dictate how the object should be drawn to screen.

We will use plotnine to draw a graph based on a subset of diamonds data, chosen by the user. And define our server function as

from shiny import render
import plotnine as gg
from plotnine.data import diamonds

# utility function to draw a scatter plot
def create_plot(data):
  plot = (
    gg.ggplot(data, gg.aes(x = 'carat', y='price', color='color')) + 
      gg.geom_point()
  )
  return plot.draw()

# wrapper function for the server, allows the data
# to be passed in
def create_server(data):
  def f(input, output, session):
  
    @output(id="out") # decorator to link this function to the "out" id in the UI
    @render.plot # a decorator to indicate we want the plot renderer
    def plot():
      color = list(input.select()) # access the input value bound to the id "select"
      sub = data[data['color'].isin(color)] # use it to create a subset
      plot = create_plot(sub) # create our plot
      return plot # and return it
  return f

server = create_server(diamonds)

Full source, named “diamonds.py”, including creating the app object is

# diamonds.py
from shiny import App, ui, render
import plotnine as gg
from plotnine.data import diamonds
import pandas as pd

# function that creates our UI based on the data
# we give it
def create_ui(data: pd.DataFrame):
  # calculate the set of unique choices that could be made
  choices = data['color'].unique()
  # create our ui object
  app_ui = ui.page_fluid(
    # row and column here are functions
    # to aid laying out our page in an organised fashion
    ui.row(
      ui.column(2, offset=1,*[
        # an input widget that allows us to select multiple values
        # from the set of choices
        ui.input_selectize(
          "select", "Color",
          choices=list(choices),
          multiple=True
        )]
      ),
      ui.column(1),
      ui.column(6,
        # an output container in which to render a plot
        ui.output_plot("out")
      )
    )
  )
  return app_ui

frontend = create_ui(diamonds)

# utility function to draw a scatter plot
def create_plot(data):
  plot = (
    gg.ggplot(data, gg.aes(x = 'carat', y='price', color='color')) + 
      gg.geom_point()
  )
  return plot.draw()

# wrapper function for the server, allows the data
# to be passed in
def create_server(data):
  def f(input, output, session):
  
    @output(id="out") # decorator to link this function to the "out" id in the UI
    @render.plot # a decorator to indicate we want the plot renderer
    def plot():
      color = list(input.select()) # access the input value bound to the id "select"
      sub = data[data['color'].isin(color)] # use it to create a subset
      plot = create_plot(sub) # create our plot
      return plot # and return it
  return f

server = create_server(diamonds)

app = App(frontend, server)

Finally running the app to see the fruits of our labour

shiny run --port 3838 diamonds.py

Screen shot of simple shiny application

We can update the chart by selecting the colours of diamonds that we want to display in the dropdown.

An example of this app that you can browse and edit is hosted here using the new shiny live. The source is also available in our blogs repository.


Jumping Rivers Logo