Thursday, March 2, 2023

Rendering matplotlib charts in Flask

Way back in the day when I was working in Google developer relations, we released one of my favorite tools: the Google Charts API. It was an image API which, with the right set of query parameters, could produce everything from bar charts to "Google-o-meters". The team even added custom map markers to the API at one point. I pointedly asked team "do you promise not to deprecate this?" before using the map marker output in Google Maps API demos, since Google had started getting deprecation-happy. They promised not to, but alas, just a few years later, the API was indeed deprecated!

In honor of the fallen API, I created a sample app this week that serves a simple Charts API, currently outputting just bar charts and pie charts.

API URLs on left side and Chart API output on right side

To output the charts, I surveyed the landscape of Python charting options and decided to go with matplotlib, since I have at least some familiarity with it, plus it even has a documentation page showing usage in Flask.

Figure vs pyplot

When using matplotlib in Jupyter notebooks, the standard approach is to use pylot like so:

import matplotlib.pyplot as plt

fig, ax = plt.subplots()
ax.pie(data["values"], labels=data.get("labels"))

However, the documentation discourages use of pyplot in web servers and instead recommends using the Figure class, like so:

from matplotlib.figure import Figure

fig = Figure()
axes = fig.subplots(squeeze=False)[0][0]
axes.pie(data["values"], labels=data.get("labels"))

From Figure to PNG response

The next piece of the puzzle is turning the Figure object into a Flask Response containing a binary PNG file. I first save the figure into a BytesIO buffer from Python's io module, then seek to the beginning of that buffer, and finally use send_file to return the data with the correct mime-type.

from io import BytesIO

# Save it to a temporary buffer.
buf = BytesIO()
fig.savefig(buf, format="png")
buf.seek(0)
return send_file(buf, download_name="chart.png", mimetype="image/png")

You can see it all together in __init__.py, which also uses the APIFlask framework to parameterize and validate the routes.

No comments: