An Interactive Data Dashboard in Pure Python with Taipy
Taipy is an innovative framework designed to simplify the creation of interactive and visually engaging data science applications
Taipy is competing in the same space as Streamlit although, as we shall see, it is quite a different beast.
In this tutorial, we will introduce the fundamental concepts of Taipy and guide you through the creation of a CO2 emissions visualization app. This example will demonstrate how to use Taipy to build an interactive application that visualizes global CO2 emissions data through a choropleth map and accompanying charts and tables.
The final app will look like the screenshot below.
As you can see there is a banner title at the top of the screen and two charts: a choropleth on the left and a bar chart on the right. The choropleth shows the total CO2 emissions for each country on the map for a particular year and the bar chart shows the emissions for the selected countries for the selected year.
Above the choropleth is a slider which allows the user to select a year and to the side of the bar chart is a multiple-select box that lets you choose countries. The slider will update both charts, while the select box only affects the bar chart.
Taipy, briefly
Taipy consists of parts: Taipy GUI and Taipy Core. We will be dealing mostly with the GUI part which can be used to create user-friendly graphical user interfaces. Taipy Core looks after how an application is run and manages the system state. In the application that we will develop, this is mostly invisible.
Fundamentally, a Taipy app consists of a GUI definition and program logic written in Python. The GUI communicates with the rest of the program through event handlers and the system state.
This arrangement sets it apart from Streamlit which re-runs the entire program each time a user interface component is changed and variables that need to retain their value must be explicitly stored in the system state. By contrast, in a Taipy app, top-level variables are implicity part of the system state and can be bound to values in the GUI (so updating one of these variables can automatically change the value in a GUI component). GUI components can also optionally have an event handler associated with them. Taipy only re-runs the necessary parts of the code when a GUI component changes. Taipy claims their approach results in apps that run considerably faster than other libraries.
The structure of a Taipy app is also different from Streamlit. Streamlit GUI components can be (and often are) embedded in the program logic of the app, whereas with Taipy the GUI is defined separately and is built around the concept of a page.
The Software Development community has regarded as good practice this separation of GUI and program logic for a long time (see, for example, Modelโviewโcontroller - Wikipedia).
In developing our app, we will be using the Taipy GUI Builder which allows you to define a GUI with Python. Taipy also supports page building in HTML and Markdown, but we won't concern ourselves with that here.
Setting Up Taipy
Before we start, ensure you have the necessary libraries installed. You can install Taipy and other required libraries using pip:
pip install taipy pandas plotly
Creating a CO2 Emissions Visualization App
So, let's dive into building a CO2 emissions visualization app with Taipy. (You can download all the code and data from GitHub - see Notes.)
First, you will need to import the libraries that we will use.
from taipy.gui import Gui
import taipy.gui.builder as tgb
import pandas as pd
import plotly.express as px
1. Data Loading and Initialization
We'll load the CO2 emissions data from a couple of CSV files and perform some simple preprocessing. We will also initialize global variables that will be used throughout the app.
# Define the global (state) data
# Load CO2 emissions data from CSV file and preprocess it
df_all_countries = pd.read_csv('co2_total.csv')
df_all_countries = df_all_countries.drop(columns=['Unnamed: 0'])
df_world = pd.read_csv('co2_total_world.csv')
df_world = df_world.drop(columns=['Unnamed: 0'])
col = 'Annual COโ emissions' # the column that contains the data
max = df_all_countries[col].max() # max emissions value for color range
min = df_all_countries[col].min() # min emissions value for color range
ymax = 2021 #df_total["Year"].max() # first year
ymin = 1950 #df_total["Year"].min() # last year
year = ymax # default year
# create a list of all countries
all_countries = list(df_all_countries['Entity'].unique())
countries = ['France','United Kingdom'] # set a default list - could be empty
# the working list is the dataframe for the countries currently selected by
# the selected year and the selected list of countries
df_working_list = df_all_countries[
(df_all_countries['Year']==year)&
(df_all_countries['Entity'].isin(countries))
]
The data that we are using is derived from Our World in Data (see Notes). For convenience I've created two files; one contains data for many countries and areas in the world over a period from as early as 1750 to 2021 and the other contains only data for the World as a whole. You can see a snapshot of the data below.
Note that not all countries have data going back to 1750 and, in our app, we will use the range 1950 to 2021.
We also set some global data to record the list of all countries in the data and an initial list of countries (that could be empty if you prefer).
We also set variables for the years and initialise the current year to the highest year recorded.
Finally, we create a 'working list', a subset of the larger dataframe that represents the data for the selected countries in the current year.
These values will allow us to draw an initial dashboard. But, first, we need some code to plot the choropleth.
2. Defining the Map Plotting Function
We could include this code in the GUI layout but it would end up being quite clumsy. A neater solution is to define a function to generate a choropleth map for a given year using Plotly Express and invoke that in the GUI code.
def plot_choro(year):
"""
Function to draw a choropleth map for the given year.
Parameters:
year (int): The year for which to draw the choropleth.
Returns:
fig: Plotly figure object for the choropleth map.
"""
fig = px.choropleth(df_all_countries[df_all_countries['Year']==year],
locations="Code", # The ISO code for the Entity (country)
color=col, # color is set by this column
hover_name="Entity", # hover name is the name of the Entity (country)
range_color=(min,max), # the range of values as set above
scope= 'world', # a world map - the default
projection='equirectangular',
title='World CO2 Emissions',
color_continuous_scale=px.colors.sequential.Reds,
)
fig.update_layout(margin={'r':50, 't':0, 'b':0, 'l':0}) # adjust the figure size
return fig
This function draws the map for a particular year and returns the figure created. The adjustments to the margins are to maximise the size of the chart - I've made the right-hand margin bigger to get a better separation between the charts.
3. Defining the GUI Layout
Using Taipy's GUI Builder library, we define the structure and elements of our app's GUI. This includes a header, a choropleth map, a slider for year selection, a country selector, and a bar chart.
The figure below shows the layout of the page (not to scale). The blue background represents the page (note that the margins between UI elements shown in the diagram are for clarity, they won't be in the final GUI).
As we can see the page is topped with a header and below that is a container which is split into two columns. Column one is divided horizontally into two with the bottom part containing the map and the top part divided into two columns; the slider to select the year on the left and the emissions total that is dynamically updated on the right.
The right-hand column is also split into columns with the country selector on the left and the bar chart on the right.
Finally, there is a footer at the bottom of the page.
We can represent this layout as a tree, as in the figure below, and this tree will map directly onto the code we need to write to implement the GUI.
page
|-- header
|-- columns
| |-- col 1
| | |--columns
| | |--col 1.1
| | | |--text
| | | |--slider
| | |--col 1.2
| | | |--text
ย ย |ย ย ย ย ย ย ย |ย ย ย ย ย ย ย ย ย ย |ย ย ย |--emissions total
| | |-- map
| |-- col 2
| |--columns
| |-- col2.1
| | |-- text
| | |-- country selector
| |-- col 2.2
| |-- bar chart
|-- footer
To begin coding the GUI, we start with a page - the top-level GUI construct - and everything else will sit inside it.
At the top of the page, we use the tgb.text
function to write the header. This is the text for the header followed by a horizontal line. The function takes a string as an argument (as you would expect) and if we set the mode
argument to 'md'
it will treat the string as Markdown, rather than simple text.
Following this we see some nested constructs that follow the same pattern as the tree we saw above.
The function tgb.layout
creates a container with columns inside the page. The first one you see with the argument columns="3 2")
creates two columns that fill the width of the screen with column widths that are in the ratio 3:2, that is the left column will be 3-fifths of the width of the page while the right-hand columns will be 2-fifths. The reason being, of course, that the map requires more room than the bar chart.
Within that layout, the next two items will be positioned in the two columns. If we were to write two text items, for example, then one would be in each column.
But we need to do something more complicated. There will be several UI components in each column. To achieve this we need to wrap those components in another container, tgb.part
as you can see following the "Column 1" and "Column 2" sections in the code below.
The rest of the UI mainly consists of text fields but, of course, in addition, there are the control elements (slider and selector) and the charts. We'll deal with these below along with the way the UI elements bind values to the previously defined variables.
# Define the page
with tgb.Page() as page:
# Header
tgb.text( "# World CO2 Emissions from {ymin} to {ymax}", mode='md')
tgb.text("---", mode='md')
# Left column contains the choropleth and the right one the data
with tgb.layout(columns="3 2"):
# Column 1: choropleth and slider to select year
with tgb.part():
with tgb.layout(columns="1 1"):
with tgb.part():
tgb.text(value="#### Use the slider to select a year", mode='md')
tgb.slider(value="{year}",min=ymin, max=ymax, on_change=update_data)
with tgb.part():
tgb.text("#### Total global emissions for {year}:", mode='md')
tgb.text("##### {int(df_world[df_world['Year']==year]['Annual COโ emissions'].iloc[0])} tonnes", mode='md')
tgb.chart(figure="{fig}")
# Column 2: A header with the global emissions per year and the data for the selected country/year
with tgb.part():
# data
tgb.text(value="#### World temperature data", mode='md')
tgb.text(value="##### Select or reject countries from the dropdown list", mode='md')
with tgb.layout(columns="1 2"):
tgb.selector(value="{countries}", lov="{all_countries}", multiple=True,dropdown=True, width=1000 ,on_change=update_data)
tgb.chart("{df_working_list}", type="bar", x="Entity", y=col)
# footer with acknowledgement
tgb.text("Global CO2 Emission Data from {ymin} to {ymax}. Data derived from [Our World in Data](https://ourworldindata.org/) (with thanks)", mode='md')
4. Bindings and callbacks
With the code above we have defined the layout and we have also defined how the UI elements communicate with the rest of the app. To explain this we need to look a little closer at those elements and how they utilise the system state and events.
Let's look at the first tgb.text
statement. As previously noted, it takes a string as a parameter and this string will be displayed in the UI.
tgb.text( "# World CO2 Emissions from {ymin} to {ymax}", mode='md')
We also see that the mode
argument tells us that the string will be interpreted as Markdown text but what are {ymin}
and {ymax
}? You may remember that these are variable names we defined earlier and the f-string type of notation refers to those variables. This means that the {ymin}
, for example, will be replaced by the value of the global variable, ymin
. This mechanism dynamically binds the values displayed in the UI to the previously defined variables.
The active elements of the UI are the controls (the slider and the selector) and the charts the map and the bar chart). These also use the same sort of binding but, in addition, the controls can invoke callback functions when they are changed. Let`s look at the slider.
tgb.slider(value="{year}",min=ymin, max=ymax, on_change=update_data)
Here we see four arguments. The first is the value that the slider will return (which is bound to the global year
), the second and third are the maximum and minimum values (the globals ymax
and ymin
), and the third is a callback function.
When the slider is moved, two things happen; the global variable year
is updated to the value returned by the slider, and the callback function updata_data
is invoked.
The change in the value of year
is immediately reflected in the parts of the UI that have values that are bound to year
. So, the panel where the total emissions per is displayed will be updated with the new value of year
.
The callback function updates the 'working list', the data we want to display in the bar graph, and redraws the map with the new year
value assigning the new map to the global variable fig
. This, of course, means that the UI element that displays the map is also updated as it uses a value that is bound to fig
.
The rest of the code uses similar methods but there are a couple of things that we should note.
The first is that bindings in the UI elements need not be simple variables, we can use expressions inside the curly brackets, too. You can see this in the tgb.text
element that displays the total global emissions for a year: this employs a pandas expression along with bindings to the global variables df_total
and year
.
The second is to note that the function tgb.chart
(which draws a Plotly chart) can be used in two ways. In the case of the bar chart, the chart type and all the required parameters are included.
tgb.chart("{df_working_list}", type="bar", x="Entity", y=col)
And this is fine when the code is short like this. However, the code to create the map requires far more parameters so it is better, in my view, to draw the figure in a separate function and pass the figure to the tgb.chart
function as follows.
tgb.chart(figure="{fig}")
You can use whichever form that you are most comfortable with.
Let's now look at the callback function.
5. Updating Data Based on User Inputs
We create a callback function to update the working list of data and the choropleth map based on the user-selected year and countries. This function will be triggered whenever the user changes the year or selected countries - both the slider and the selector invoke the same callback function.
Often, you will want to use the value returned by a control in a callback and these are automatically passed as parameters.
def typical_callback_fn(state, var, val):
pass
The first parameter state
gives access to the system state - we'll see how this is used below. The parameters var
and val
refer to the value parameter in the control that has invoked the callback. They are its name and value, respectively.
Our callback code is shown below. As already mentioned both the slider and the country selector invoke this callback as we want to update the entire UI when either changes. We do this by updating the working list (the data for the currently selected countries and currently selected year) and the choropleth.
To update these values, we need to correctly access the global variables and this is where the parameter state
comes in. This gives us access to a Taipy-defined object that contains the state of the app and we refer to the variables as attributes of the state object.
def update_data(state):
"""
Function to update the working list and plot the choropleth when a new year or new country is selected.
Parameters:
state (object): The state object containing the application's current state from which we can
find the currently selected year, country list and dataframe
"""
state.df_working_list = state.df_all_countries[
(state.df_all_countries['Year']==state.year)&
(state.df_all_countries['Entity'].isin(state.countries))
]
state.fig = plot_choro(state.year)
The only thing left is to run the app!
6. Running the App
Finally, we run the app by creating a Gui
object with the defined page and calling its run
method.
# Run the app
Gui(page=page).run()
This runs the app locally in the browser. Taipy also provides a server environment for deployment to the cloud but we'll look at that another day.
Conclusion
This tutorial has introduced you to Taipy, an innovative framework for building interactive and visually appealing data-driven applications. Through the example of a CO2 emissions visualization app, we demonstrated how to use Taipy to load and preprocess data, create dynamic plots, manage state and user interactions, and design a responsive and user-friendly interface.
Investigating ways of creating web apps for data visualizations is, it seems, a never-ending task. My default method used to be Flask and HTML, I flirted with Dash but wasn't that enthusiastic (I should take another look, perhaps) then along came Streamlit, of course. And, to be sure, there are more apps and platforms that deserve attention.
Taipy, looks like a good candidate for data visualization. It is based around Flask which seems to give it a performance advantage over Streamlit - it doesn't re-run the entire app when something changes, I guess that helps.
I like the separation of GUI from the rest of the code and while my first inclination was to use the GUI Builder library to create the UI in Python, I intend to look at the other methods, too. The Markdown method seems to feature most on the Taipy website and while slightly arcane-looking, it does seem to result in compact code.
But that's a job for another day.
Thanks for reading and I hope you enjoyed this brief look a Taipy. Please do download the code and data (link below) and give it a go yourself. Any opinions, criticisms or any other comments are always welcome.
Notes
Our World in Data publishes articles and data about the most pressing problems that the world faces. All of its content is open source and its data is downloadable - see About - Our World in Data for details.
Code and data for this app are available on [GitHub](GitHub - alanjones2/taipyapps) in the folder 'dashboard'.