Hello Shiny Python
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.
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
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.