By combining Mesop’s simple yet powerful UI components with Plotly’s comprehensive charting capabilities, you can build a dynamic, interactive app to showcase data visualizations in Python.

Mesop is a Python-native framework developed for rapid AI and web app development that allows you to build sophisticated interfaces without the need for traditional frontend skills; Plotly is, of course, a well-known and well-used graphing library.

We’ll take these components and build a data visualization app.

Mesop

Mesop is not an officially supported Google product but it is well-documented, used internally within Google and is receiving a fair amount of publicity, so I think we can take it seriously. Mesop is an open-source project licensed under the Apache-2.0 license [1].

Here is a ‘Hello World’ app in Mesop.

import mesop as me

@me.page(path="/")
def app():
  me.text("Hello World")

Hello World with Mesop

As you can see it has a structure with a route defined as a function with a decorator that specifies the endpoint. If you are familiar with Flask, you’ll be at home with Mesop - indeed Mesop is based upon Flask. Look at the Flask equivalent of the code above and you can see the similarities.

from flask import Flask

app = Flask(__name__)

@app.route('/')
def app():
    return "Hello World"

Hello World with Flask

Unlike Flask, however, Mesop does not use HTML templates. Instead, Mesop provides ready-to-use UI components, including buttons, menus, tables and other useful components including AI-specific features like a chat interface.

Developing with Mesop

We are going to develop a multi-page, interactive data visualization application and on the way we’ll introduce various aspects of Mesop that are valuable for developing data visualization apps.

The final app with be an exploration of global CO2 emissions that will use buttons, a drop-down menu, a slider and interactive Plotly charts. You can see the main page below. There will be two more pages which will look like the thumbnails on the home page and they will contain a bar chart that shows the CO2 emissions by source and a choropleth that shows emissions by country. (The data used in this app is derived from Our World in Data[2] and included in the project repository).

First, we need to set up the environment.

It’s good practice to use a virtual environment when starting a new project. I use Conda for this so my first step is to go to a command prompt, create a new project directory and create the environment.

conda create -n mesop python=3.12

That will create a new environment for me that uses Python 3.12 called mesop.

I then activate it, install mesop and run VScode (where I do my programming) from the current directory.

conda activate mesop
pip install mesop
code .

If you want to run the code here, you also need to install plotly and pandas.

Mesop apps are web apps. You start them with the command mesop, e.g.

mesop app1.py

After running the code you will see this message:

Running server on: http://localhost:32123
 * Serving Flask app 'mesop.server.server'
 * Debug mode: off

Your application is now running on the localhost port 32123. Open that in your browser and you will see the result - if you run the ‘Hello World example’ above, you’ll see that message on a webpage.

Using Mesop components

We need to know a little more about Mesop to use it effectively. We need to be able to use its UI components, how to process user input and also how to use state variables (something that will be familiar to Taipy or Streamlit users).

So, to get started, let’s look at an app that uses all of these things but is still very simple. Below is a screenshot of a click-counter app that displays the number of times that a button is clicked. Following that is the code.

In this app we have some simple UI components - a header, some text and a button - code that captures clicks on the button, and updates the text, and a class that records the state of the app. Let’s see the code.

import mesop as me

@me.stateclass
class State:
  clicks: int

def button_click(event: me.ClickEvent):
  state = me.state(State)
  state.clicks += 1

@me.page(path="/counter")
def main():
  state = me.state(State)
  me.markdown("# Click counter")
  me.markdown(f"Clicks: {state.clicks}")
  me.button("Increment counter", type="flat", 
            on_click=button_click)

This app has three parts and illustrates the main aspects of a Mesop app. We need somewhere to record the number of clicks and this is done by using a class called State. That is the first thing that we create and it contains a single integer variable, clicks.

The second part of the app is a function that updates the record of clicks. This is invoked when the button is pressed and increments the clicks attribute in state which is an instance of the class State.

The final part of the app defines the endpoint and the user interface at that endpoint. First, we create a local version of the state object, then we go on to code the UI.

We can see three statements that build the UI. Two markdown strings and a button. The UI components are within the Mesop library and so are preceded with the prefix me. . me.markdown() takes a markdown string and renders it on the screen. The first of these is a simple header. The second displays a Python f-string, which allows us to embed the value of state.clicks. Finally, the button component: it takes three parameters, a label, a type and a reference to an event handler - when the on_click event occurs, the function button_click is invoked.

All Mesop components are prefixed with me. and we can just call them in the right order to create our UI. We’ll see more examples of components, later.

But to design a good app we need to think about layout.

Mesop layout

The fundamental layout component in a Mesop app is the box. By combining boxes with the appropriate styling, we can create rich layouts using rows and columns, grids, or any arbitrary layout that we want.

Let’s say that, in the Click Counter, we want to put the text and the button in the same row. We can just put them into a box and set its layout to flex, (this is a CSS attribute that means that the components will be placed on a single line, if possible). We do this by setting the style parameter as below, with the me.Style() function.

  me.markdown("# Click counter")
  with me.box(style=me.Style(display="flex", gap=20)):
    me.markdown(f"Clicks: {state.clicks}")
    me.button("Increment counter", type="flat", 
            on_click=button_click)

This will put the components on the same line (note we set a gap of 20 so there is some space between them.) Alternatively, we could lay the components out as a grid.

  with me.box(style=me.Style(display="grid", 
                             grid_template_columns="1fr 1fr")):
    me.markdown(f"Clicks: {state.clicks}")
    me.button("Increment counter", type="flat", 
            on_click=button_click)  

In this code, the number of columns is defined by the number of items in the grid_template_column string. And the values define the relative widths of the columns. So, “1 fr 1fr” means two columns of equal width, whereas “2fr 1fr 1fr” would mean that there are three columns with the first one being twice the width of the other two.

Writing lots of style parameters for each box that you use can get quite tedious and, since It’s quite a good idea to limit the number of styles that you use, defining them once and then reusing them is a good idea. Below I have defined a style that I will use as the default for future layouts.

# Define a style
BLOCK_STYLE = me.Style(
              padding=me.Padding.all(12),
              background="white",
              width="100%",
            )

 # and use it
 with me.box(style=BLOCK_STYLE):
   # components here

This ensures that the width of the box fills the width of the window but that there is a 12-pixel padding around it and that the background is white. Now everything inside that box will use that style unless the style parameters are overridden and we can reuse the style as I do on the three pages that will make up our app.

There is a lot more to styling than this. The parameters that are set in me.Style() are equivalent to CSS styles and there are lots of them, so I’ve limited what I shall cover here to what we will need for this little project.

Data Visualization App

We could talk about Mesop components and layouts for a very long time but that is not why we are here. Our aim is to create a data visualization app and we have covered enough of the basics of Mesop to let us get on with it.

The first thing to admit is that Mesop does not natively support Plotly. But don’t panic! This is a very small hurdle that is easily overcome. Mesop does have a me.plot() component but that is restricted to Matplotlib. Mesop also has a way to create user-defined components and, while I have not looked closely at that, it seems like quite a long-winded way of solving the simple problem of using Plotly.

All you need to use Plotly is a small amount of boiler-plate HTML code that can be reused, as is, for any Plotly project. And since Mesop does have a me.html() component, that is what I intend to do.

Below is a reusable function that will display a Plotly chart.

def chart_html(chart):
        return f"""
        <div id='figure'></div>
        <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
        <script>
        Plotly.react('figure', {chart});
        </script>
        <div id='figure'></div>
        """

It’s simply a bit of HTML into which we insert the Plotly chart that we want to display. We can then use an HTML component to display it on the page.

Let’s see it in use.

import mesop as me
import plotly.express as px

#
# Defs for BLOCK_STYLE and chart_html() go here
#

@me.page(path="/")
def main():
  with me.box(style=BLOCK_STYLE):
    me.markdown("# A Plotly chart")
    data_canada = px.data.gapminder().query("country == 'Canada'")
    fig = px.bar(data_canada, x='year', y='pop')
    chart = chart_html(fig.to_json())
    me.html(
        chart_html(chart),
        mode="sandboxed",
        style=me.Style(width="100%", height="500px")
    )

We have the same structure as before and we put everything in a me.box(). The Plotly chart is adapted from the Plotly documentation and uses the bundled Gapminder data to display a population chart for Canada. To include this in the HTML code we need to convert it into JSON format, pass it to the previously defined function and render it using a call to me.html().

The style setting code is self-explanatory but the mode parameter needs to be explained. While Mesop allows HTML code it will filter out anything that might be dangerous like JavaScript code. If we need such code we have to set the mode to “sandboxed”. This means that the HTML will be rendered safely in an iframe.

Below is a screenshot of the result.

Right, now we have a chart it feels like we are on our way!

CO2 Data

We will be using CO2 emissions data as I’ve already mentioned and to make this easy, I’ve defined a Python class that reads the data and provides methods that will return the required Plotly charts in JSON format. The code for this is included in the project GitHub repository but we won’t go into any detailed explanation of the implementation here (the code is easy to follow). What we need to know is that we can instantiate a CO2 data object and

import CO2_Data as CO2

co2_data = CO2.CO2_Data()
bar_chart = co2_data.plot_chart("Total")

We will see exactly how it is used later but there are two main methods:

  • plot_chart: this returns a bar chart of CO2 emissions from a particular course over time. The sources are ‘Total’, ‘Oil’, ‘Cement’, ‘Flaring’ and ‘Other’ and we need to provide the source as a parameter.

  • plot_choro: this returns a world map of total CO2 emissions for a particular year with the countries coloured according to the level of emissions. It takes the year as a parameter.

We also use some attributes that are defined in the class.

We can now concentrate on the UI in Mesop.

Application Pages

I’ll deal with them separately but I’ve coded all the pages in a single file. This is the simplest method but not necessarily the best. If there were more than three pages, I’d probably code each page in its own file. Here is the layout for all of the pages.

@me.page(path="/")
def home():    
    # Home page code goes here

@me.page(path="/CO2")
def CO2():
    # CO2 sources page code goes here

@me.page(path="/map")
def map():
    # Map page code goes here

Each page is given a route in the @me.page decorator and the code for the page is defined in the following function.

We’ll start at the beginning with the home page.

def home():    
    with me.box(style = BLOCK_STYLE):       
        banner('World CO2 Emissions','Explore the graphs below')
        me.markdown( """The World's CO2 emissions have been increasing at an alarming rate since the time of the Industrial Revolution. 
                    We demonstrate the extend of those emissions using two data sets:  the first tracks overall emissions and we show those
                    onto a World map. We also show the breakdown of the sources of those emissions in a set of bar graphs.""")
        me.markdown("_Click on the buttons below to see the charts._")

        with me.box(style = FLEX_STYLE):
            with me.box():
                me.image(src = "https://github.com/alanjones2/mesopapps/blob/main/CO2/images/Co2sources.png?raw=true",
                    alt="",
                    style=me.Style(width="90%")
                )
                me.button("By source", type='flat',on_click=navigate, key='CO2')
            with me.box():
                me.image(src = "https://github.com/alanjones2/mesopapps/blob/main/CO2/images/Co2country.png?raw=true",
                    alt="",
                    style=me.Style(width="90%"),
                )
                me.button("By country", type='flat', on_click=navigate, key='map')

The whole thing goes in a me.box styled with BLOCK_STYLE and so fills the whole page but includes padding so that the page content does not reach the extremities of the window.

The content is mostly static. We have a banner, some text formatted as Markdown and then boxes that each contain images and a button that will select a new page. These boxes are contained in a third box styled with FLEX_STYLE which attempts to ensure that the image/button boxes are displayed on the same line.

Most of this is self-explanatory but we need to focus on two things. The first is banner. This is a function that will be used by all of the pages and is in utils.py (so this needs to be included). This takes two parameters, a heading and a sub-heading (which can be blank) and is styled with BANNER_STYLE from styles.py.

def banner(head='', subhead=''):
    with me.box(style=BANNER_STYLE):
        me.markdown( f"# {head}", style=me.Style(color="Navy"))
        me.markdown( f"## {subhead}", style=me.Style(color="SteelBlue"))
    return banner

From a functional point of view, the more important thing to look at is the button code. This allows us to navigate to a new page. It works in the same way as the button we saw in the Click Counter code and calls a function navigate. But there is an additional parameter key that we set with a string that represents the page that we want to navigate to. Here in the navigate function

def navigate(event: me.ClickEvent):
    match event.key:
        case "CO2":
            me.navigate("/CO2")
        case "map":
            me.navigate("/map")
        case _:
            me.navigate("/")

As you can see key is part of an event object. We read it and depending on its value, use the built-in me.navigate() function to go to the required page. Each of the other pages also uses the same function to navigate to the home page.

The map page and the CO2 sources page are logically very similar. Here’s the code for the map page.

def on_slider_change(event):
   global CO2_map
   State.year=int(event.value)
   CO2_map = co2_data.plot_choro(int(event.value))   

@me.page(path="/map")
def map():
    with me.box(style = BLOCK_STYLE):
        banner( f"CO2 Emissions by country for {State.year}")
        me.slider(on_value_change=on_slider_change, value=State.year, max='2020', min='1900', discrete=True)

        me.button("Home page", on_click=navigate, key='home')
        me.html(
            chart_html(CO2_map),
            mode="sandboxed",
            style=me.Style(width="100%", height="500px")
    )

Another box and another banner. But then a me.sliderfunction with parameters whose use is clear. When it changes the global variable CO2_map is updated and this is displayed via the me.html() function and our little utility chart_html . We also see that the button uses the navigate function to return to the home page when pressed.

The rest of the code is similar to things we’ve seen before.

That leaves the CO2 Sources page which uses a drop-down menu to select the source to be displayed.

def on_selection_change(select):
   global bar_chart
   bar_chart = co2_data.plot_chart(select.values[0])

@me.page(path="/CO2")
def CO2():
    with me.box(style = BLOCK_STYLE):
        banner( "CO2 Emissions by source since the mid-19th Century")
        me.select(
            label="Select a CO2 Source",
            options=[
            me.SelectOption(label="Total", value="Total"),
            me.SelectOption(label="Coal", value="Coal"),
            me.SelectOption(label="Oil", value="Oil"),
            me.SelectOption(label="Cement", value="Cement"),
            me.SelectOption(label="Flaring", value="Flaring"),
            me.SelectOption(label="Other", value="Other"),      
            ],
            on_selection_change=on_selection_change,
            style=me.Style(width=240, height=70),
        )

        me.button("Home page", on_click=navigate, key='home')
        me.html(
            chart_html(bar_chart),
            mode="sandboxed",
            style=me.Style(width="100%", height="500px")
    )

The logic is the same as before so I won’t go into detail. The me.select function has different parameters to the slider but the main thing to notice is that the possible selections are defined as a list of me.SelectOption() and that each of these has a label (that will be displayed) and a value which is passed to the change function.

Conclusion

There we have the complete app based on Mesop.

I’ve tried to make this as clear as I can without embedding the whole of the code into the article. If it is not absolutely clear how we put together all of the sections then you should refer to the code in the GitHub repo and read the README.md file there.

So, what do we think about Mesop as a vehicle for creating visualisation app? All new frameworks represent a learning curve but Mesop is not that difficult if you are familiar with other Flask-based frameworks such as Dash or Taipy.

Mesop’s components seem easy to use but while the layout is simple, it can get a bit verbose. This is down to the fact that the layout parameters are basically a mirror of CSS attributes that you find in HTML. However, the ability to define layouts and use them multiple times is a useful way of reducing verbosity and providing consistency.

I’ve found working with Mesop for the first time a useful experience and I am likely to use it again. I might not code everything in the same way next time (I’m always uncomfortable with using global variables, for example, and I might put pages in separate files, in the future).

Will it become a properly supported product in the future? I have no inside information but my guess would be yes. Google seem to have put a fair amount of effort into developing it, documenting it and talking about it and it would be a shame if it disappeared.


As ever, thanks for reading, I hope that this stroll through Mesop’s abilities has been useful. All the code and data are in my GitHub repo — the app code is in the CO2 folder and the smaller experiments are in the samples folder.You can see more of my articles on my website and can find out when I publish new stuff by subscribing to my occasional newsletter. Most of my stuff is also on Medium (paywalled).


Notes and references

  1. The Mesop GitHub repository is here and the official docs are here.
  2. The data used in this article is derived from Our World in Data. OWID publishes articles and data about the most pressing problems that the world faces. All its content is open source and its data is downloadable — see About — Our World in Data for details.
  3. The title image was created with the help of DALL-E the others are all screenshots by me, the author.
  4. I have no connection with Google other than as a user.