Rational UI Design with Streamlit | by Alan Jones | Towards Data Science

When Tim Berners-Lee first invented the web, pages were just text. When the first graphical browsers like Mosaic came along web pages got pictures, too.
Later they became apps with server-side programs supplying the data to construct the web pages. This often entailed writing code in the within the HTML in PHP, or Java or Visual Basic, which the server executed in order to construct the page.
The result was often a real dog’s dinner — the code scattered throughout the HTML wasn’t clear because it was fragmented and, on top of that, it also obscured the page layout.
Those who thought deeply about these things suggested that we should separate the logic of an app from the way it was presented. One idea was the Model, View, Controller method (MVC). Here the Model was the app logic, typically some data and program code on a server; the View was how the web page should look, HTML and CSS in the browser; and the Controller was the code that connected the Model to the View, getting user input from the web page, supplying it to the Model and getting results back to display in the View.
And if that sounds complicated... well, what can I say - yes, it is a bit complicated.
Complicated because while the idea of MVC is simple enough, the technologies and techniques required mean that a whole bunch of skills are needed. For the View, HTML and CSS; for the Model, a language like Python, Java or C# (and probably some SQL, too); and for the Controller, probably Javascript, a knowledge of HTTP and, these days, Ajax, too.
Maybe you’ve never heard of MVC but many web app frameworks were (indeed, are) based on the idea.
So Streamlit is going backwards by mixing the code and the layout in a single entity. But, while that is true, it is also attempting to make life easier because all of those technologies I mentioned above are no longer necessary — all you need is Python and the Streamlit library.
So, how do we get the benefit of the simple Pythonic approach without getting mired in the complications of mixing app logic and page design?
The solution is to keep the MVC approach in mind and separate the program logic from the way it is presented.
Both Python and Streamlit can help us.
We can modularise the app using Python classes and functions, separating the View from the Model, and we can structure the View using Streamlit functions. (Streamlit has a nice recent addition: you can use the with statement with containers objects like st.container or st.columns and this allows you to show the structure of the page.)
The Controller part of MVC is taken care of by Streamlit and is incorporated into the View.
Let me show you what I mean.
Here is a simple template from which we can construct a MVC-style app in Streamlit.
############################################### A simple model/view template for Streamlit ###############################################import streamlit as st################ Model ################class Model:   caption = "This is some text"################ View  ################def view(model):   st.write(model.caption)################ Start  ################view(Model())
As you can see there are four sections: the imports, a class called Model, a function called view and the final line which calls the view function with an instantiation of Model as a parameter.
This is as simple as it can get. The Model class has a single attribute, caption, and the view function uses this to write to the screen.
For clarity, the last line is the equivalent of creating an object of type Model and passing it to the view function. Something like this:
m = Model()view(m)
Now let’s imagine we want to design a simple app that looks like the image at the top of this article. It’s what has become a classic illustration of how GDP per Capita relates to life expectancy and is based on the work of the late Hans Rosling from Gapminder (and if you don’t know about Gapminder or Hans Rosling, be sure to follow the link).
The layout of the app looks like this:
This is the basis of our View: a header, below which are two columns; the right hand column contains the chart that we want to display; the left hand column contains some text that describes the page and, below that, a slider that allows the user to select a year to be displayed on the chart.
Here is a sort of pseudo code of what the Streamlit View would look like
# Headerst.header(caption)# Two columns: #   1. Commentary and slider#   2. Main chartcommentaryCol, chartCol = st.columns(dimensions)with commentaryCol:   # Show commentary text   # Show year Sliderwith chartCol:   # Display Main Chart
You can see, I hope that the code will mirror the layout diagram — we just need to fill in the detailed code.
Before we do that though we also need to consider what data the View requires. And that is four things: the caption for the header, the commentary text, the parameters that the slider will require and, finally, the chart to be displayed.This data will be provided by the Model.
Let’s start looking at the code.
The imports are as follows:
import streamlit as stimport pandas as pdimport plotly.express as px
We need Streamlit (of course), Pandas to hold the Gapminder data, and Plotly to draw the chart.
The model should come next in the program file but we are going to park that for now and look at how we develop the View.
The View is a function called view which contains all of the Streamlit code to create the web page. We pass a instance of the model to it and all of the data that Streamlit needs will come from there. So, for example, the header caption is held in the Model in an attribute called header, thus the Streamlit code to write the header will be st.header(model.header).
Here is the complete code which is a development of the pseudo code that we saw earlier.
################ View  ################def view(model):   st.set_page_config(layout = 'wide')   # Header   st.header(model.header)      commentaryCol, spaceCol, chartCol=st.columns((2,1,6))   # Description   with commentaryCol:      st.write(model.description)   # Year Slider     year=st.slider(model.sliderCaption,      model.yearStart, model.yearEnd,      model.yearStart, model.yearStep)   #Chart   with chartCol:      st.plotly_chart(model.chart(year),          use_container_width = True)
Notice that I am using the with statement to show which UI element the following code is for. This is a nice touch that was included in a recent release of the Streamlit library and makes the code easier to read.
The first thing in the code is to set the page layout to wide. This is not essential but I think the end result looks better.
Next follows the main elements:
the header: this just displays model.header at the top of the page
a set of columns: there are three, the right-hand to hold the chart, the left-hand one to hold the description and the slider and one in between which just puts a bit of space between the other two.
the description goes in commentaryCol and just writes out model.description
the slider that we will use to select the year also goes in commentaryColbelow the description. The slider requires 5 parameters, a caption, start and end points, the initial value and a step. These are provided by the model attributes as shown in the code.
the chart goes in chartCol and is a call to st.plotly_chart. The actual chart to be displayed is returned from a method in model called chart. This method needs the year to be passed as a parameter.
The final code is the same as we saw earlier: it calls view with an instantiation of Model as a parameter.
################ Start  ################view(Model())
In the Model we need to provide all of the data that the view requires. Much of that is done in the constructor method __init__. This method is called when an object is created from Model. The first thing to do is to load the Gapminder data into a Pandas Dataframe — the Gapminder data is included in the Plotly library and so we are using this; in a real app this data would likely be loaded from a database or from an API.
From the Gapminder data we set a number of attributes, a list of unique years (which we convert from their original Numpy types to plain old Python int which Streamlit seems to prefer).
We then set the start, end and step values that the slider will use.
Next comes the chart method which draws a Plotly scatter chart.
Lastly, we set attributes for the various string that the View will use.
################ Model ################class Model:   def __init__(self):      self.df = pd.DataFrame(px.data.gapminder())      self.ylist = [int(i) for i in self.df['year'].unique()]      self.yearStart = self.ylist[0]      self.yearEnd = self.ylist[-1]      self.yearStep = self.ylist[1]-self.ylist[0]   def chart(self,year):      return px.scatter(self.df[self.df['year'] == year],         x = 'lifeExp', y = 'gdpPercap',          title = f'Year: {year}',         color='continent',size='pop')   header = 'Global Statistics from Gapminder'   description ='''      See how life expectancy changes over time       and in relation to GDP.      Move the slider to change the year to display.   '''   sliderCaption='Select the year for the chart'
If you now put those sections of code together in the right order, you will have a complete Streamlit app.
We now have an app with a separate View and Model. If we wanted to change the way the app looked then there should be no need to touch the model code, at all, only the view function.
This makes updating and maintaining the code easier and also makes the code easier to read and understand. And if, for some reason, we decide that we want to get our data from elsewhere, it is only the Model code that needs to be changed not the view.
This is a fairly trivial example but I hope that you can see that this rational approach would scale up to more complex apps and make them easier to write and maintain.
I have one doubt about the example that I have given: should the values that the slider need be calculated in the View or, where I’ve put them. in the Model? The way I’ve done it makes for a cleaner View but there is an argument that those values are only calculated because of the way we use the slider, so it might be more appropriate to derive them there.
I’d be very happy to hear how you feel about this approach, so please feel free to comment below.
As ever thanks for reading and if you would like to know when I publish new articles, please consider signing up for an email alert here.
The code for this article is easily cut and pasted from here but is also available on my GitHub page. A demonstrator page is also available here — select Rational UI Design from the drop down menu.
If you are not a Medium subscriber, how about signing up so you can read as many articles as you like for $5 a month. Sign up here and I’ll earn a small commission.