Last active
August 15, 2020 21:00
-
-
Save ryxcommar/f6d2ab7a8dc86cc0c346b3b9c9ea3967 to your computer and use it in GitHub Desktop.
How to render static Matplotlib in Flask
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"""Static Matplotlib Figures in Flask - Tutorial | |
To run: | |
1. Copy this file. | |
2. Make sure the dependencies are installed (see after the docstring) | |
3. Run in the command line: >>> python mpl_in_flask.py | |
4. Fire up your browser and view the site (default: http://127.0.0.1:5000/) | |
If you want to implement this solution: | |
1. Copy+paste ``matplotlib.use('agg')`` into whatever file your ``app`` or | |
``create_app`` is defined in. Note that this MUST be above any imports of | |
``pyplot``. Also, you only need to do this once. | |
If you ever experience any weird ``UserWarning``'s coming from Matplotlib, | |
your first troubleshooting step should be to make sure that | |
``matplotlib.use('agg')`` is being run prior to ``import matplotlib.pyplot``. | |
2. Copy+paste ``mpl_to_html``, ``_mpl_to_png_bytestring``, and ``render_mpl`` | |
into your app/blueprint file (for smaller websites) or a utils file or folder | |
(for larger websites). | |
3. Copy+paste the ``close_mpl_plot`` function plus its decorator into the file | |
or ``create_app`` function in which you define the Flask ``app``. | |
4. (Optional) Add ``render_mpl`` to the Jinja environment. This allows for | |
Matplotlib figures and subplots to be passed into ``flask.render_template`` | |
calls and then rendered using ``render_mpl``. | |
The functions are self-contained and do not use any module-level imports, so you | |
only need to copy paste the functions and not any other stuff. | |
~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ | |
Please note: This is not an amazing solution to the problem of rendering | |
Matplotlib figures in Flask, but that's because getting an amazing solution | |
requires lots of idiosyncratic setup depending on the use case and your app's | |
code structure. | |
This solution is designed to be a generalizable, portable, and lightweight | |
approach. It's more complete than anything you'll find on StackOverflow, but | |
that doesn't mean it's truly complete. | |
I don't foresee any problems with this code, but caveat emptor regardless. | |
More advanced solutions will depend on your Flask app's setup. Please read this | |
blog post for more information and for better solutions if you want to put in | |
some elbow grease: | |
https://ryxcommar.com/<<FORTHCOMING>> | |
""" | |
# !pip install numpy | |
# !pip install matplotlib | |
# !pip install seaborn | |
# !pip install flask | |
from flask import Flask | |
app = Flask(__name__) | |
################################################################################ | |
## Below lies all the stuff you'll need to copy and paste. | |
################################################################################ | |
# (1) | |
import matplotlib | |
matplotlib.use('agg') # This needs to happen before any pyplot import!!! | |
# (2) | |
def _mpl_to_png_bytestring(fig): | |
"""This function uses Matplotlib's FigureCanvasAgg backend to convert a MPL | |
figure into a PNG bytestring. The bytestring is not encoded in this step. | |
""" | |
import io | |
import matplotlib.pyplot as plt | |
from matplotlib.backends.backend_agg import FigureCanvasAgg | |
if isinstance(fig, plt.Axes): | |
fig = fig.figure | |
output = io.BytesIO() | |
FigureCanvasAgg(fig).print_png(output) | |
return output.getvalue() | |
def mpl_to_html(fig, **kwargs): | |
"""Take a figure and render it directly to HTML. A PNG is created, and | |
then encoded into base64 and decoded back to UTF-8 so that it can be stored | |
inside a <img> HTML tag. | |
``kwargs`` supports arbitrary HTML attributes. An example usage of kwargs: | |
>>> render_mpl(fig, style='width:480px; height:auto;') | |
""" | |
from flask import Markup | |
import base64 | |
bstring = _mpl_to_png_bytestring(fig) | |
png = base64.b64encode(bstring).decode('utf8') | |
options = ' '.join([f'{key}="{val}"' for key, val in kwargs.items()]) | |
return Markup(f'<img src="data:image/png;base64,{png}" {options}>') | |
def render_mpl(fig): | |
"""This function returns a png file from a Matplotlib figure or subplots | |
object. It is designed to be at the bottom of an endpoint function; instead | |
of returning HTML or ``render_template()``, you return this instead. | |
""" | |
from flask import Response | |
return Response(_mpl_to_png_bytestring(fig), mimetype='image/png') | |
# (3) | |
# This should go inside the file or function you declare your app = Flask(...). | |
@app.after_request | |
def close_mpl_plot(response): | |
"""This prevents memory leakage; Matplotlib's pyplot API is stateful, which | |
can be a burden for a website that runs for a while. | |
""" | |
import matplotlib.pyplot as plt | |
plt.close('all') | |
return response | |
# (4) | |
# Not used in this example, but it's a nice-to-have: | |
app.jinja_env.globals.update(mpl_to_html=mpl_to_html) | |
################################################################################ | |
## ... And that's it! No more copy+pasting required. :) | |
## | |
## Everything below is just an example of how to use the above functions. | |
################################################################################ | |
@app.route('/') | |
def index(): | |
# Source: | |
# https://matplotlib.org/gallery/lines_bars_and_markers/simple_plot.html | |
import numpy as np | |
import matplotlib.pyplot as plt | |
t = np.arange(0.0, 2.0, 0.01) | |
s = 1 + np.sin(2 * np.pi * t) | |
fig, ax = plt.subplots() | |
ax.plot(t, s) | |
ax.set( | |
xlabel='time (s)', | |
ylabel='voltage (mV)', | |
title='About as simple as it gets, folks' | |
) | |
# Now time for the HTML. Note how the ``fig`` generated above is wrapped | |
# with the ``render_mpl`` function. | |
from flask import url_for | |
return '\n'.join([ | |
'<!DOCTYPE html>', | |
'<head><title>Hello World MPL in Flask</title></head>', | |
'<body>' | |
'<p>Hello, world!</p>', | |
'<p>This is regular old HTML. This MPL figure is also dumped directly ' | |
"into the HTML, meaning there's no URL for it:", | |
'<br />', | |
mpl_to_html(fig), | |
'<p>See how easy that was?</p>', | |
'<p>Here is another example, but done a bit differently: this one has ' | |
'an endpoint associated with it, and it was made using Seaborn ' | |
'instead of base Matplotlib:</p>', | |
'<img src="{}">'.format(url_for('seaborn_example')), | |
"<p>Don't be shy: You can inspect the source code of this page!</p>", | |
'</body>' | |
]) | |
@app.route('/seaborn.png') | |
def seaborn_example(): | |
# Source: | |
# https://seaborn.pydata.org/examples/simple_violinplots.html | |
import numpy as np | |
import seaborn as sns | |
sns.set() | |
# Create a random dataset across several variables | |
rs = np.random.RandomState(0) | |
n, p = 40, 8 | |
d = rs.normal(0, 2, (n, p)) | |
d += np.log(np.arange(1, p + 1)) * -5 + 10 | |
# Use cubehelix to get a custom sequential palette | |
pal = sns.cubehelix_palette(p, rot=-.5, dark=.3) | |
# Show each distribution with both violins and points | |
graph = sns.violinplot(data=d, palette=pal, inner='points') | |
return render_mpl(graph) | |
if __name__ == '__main__': | |
app.run() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment