Using Flask for Scientific Web Applications

Avoiding plot files

Files with plots are easy to deal with as long as they are in the static subdirectory of the Flask application directory. However, as already pointed out, the previous vib1 app, which writes plot files, is not suited for multiple simultaneous users since every user will destroy all existing plot files before making a new one. Therefore, we need a robust solution for multiple users of the app.

The idea is to not write plot files, but instead return the plot as a string and embed that string directly in the HTML code. This is relatively straightforward with Matplotlib and Python. The relevant code is found in the compute.py file of the vib2 app.

PNG plots

Python has the io.StringIO object for writing text to a string buffer with the same syntax as used for writing text to files. For binary streams, such as PNG files, one can use a similar object, io.BytesIO, to hold the stream (i.e., the plot file) in memory. The idea is to let Matplotlib write to a io.BytesIO object and afterwards extract the series of bytes in the plot file from this object and embed it in the HTML file directly. This approach avoids storing plots in separate files, at the cost of bigger HTML files.

The first step is to let Matplotlib write the PNG data to the BytesIO buffer:

import matplotlib.pyplot as plot
from io import BytesIO
# run plt.plot, plt.title, etc.
figfile = BytesIO()
plt.savefig(figfile, format='png')
figfile.seek(0)  # rewind to beginning of file
figdata_png = figfile.getvalue()  # extract string (stream of bytes)

Before the PNG data can be embedded in HTML we need to convert the data to base64 format:

import base64
figdata_png = base64.b64encode(figdata_png)

Now we can embed the PNG data in HTML by

<img src="data:image/png;base64,PLOTSTR" width="500">

where PLOTSTR is the content of the string figdata_png.

The complete compute.py function takes the form

def compute(A, b, w, T, resolution=500):
    """Return filename of plot of the damped_vibration function."""
    t = linspace(0, T, resolution+1)
    u = damped_vibrations(t, A, b, w)
    plt.figure()  # needed to avoid adding curves in plot
    plt.plot(t, u)
    plt.title('A=%g, b=%g, w=%g' % (A, b, w))

    # Make Matplotlib write to BytesIO file object and grab
    # return the object's string
    from io import BytesIO
    figfile = BytesIO()
    plt.savefig(figfile, format='png')
    figfile.seek(0)  # rewind to beginning of file
    import base64
    figdata_png = base64.b64encode(figfile.getvalue())
    return figdata_png

The relevant syntax in an HTML template is

<img src="data:image/png;base64,{{ results }}" width="500">

if results holds the returned object from the compute function above.

SVG plots

Inline figures in HTML, instead of using files, are most often realized by XML code with the figure data in SVG format. Plot strings in the SVG format are created very similarly to the PNG example:

figfile = BytesIO()
plt.savefig(figfile, format='svg')
figdata_svg = figfile.getvalue()

The figdata_svg string contains XML code text can almost be directly embedded in HTML5. However, the beginning of the text contains information before the svg tag that we want to remove:

<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
  "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Created with matplotlib (http://matplotlib.sourceforge.net/) -->
<svg height="441pt" version="1.1" viewBox="0 0 585 441" ...

The removal is done with a little string manipulation:

figdata_svg = '<svg' + figfile.getvalue().split('<svg')[1]

Now, figdata_svg can be directly inserted in HTML code without any surrounding tags (because it is perfectly valid HTML code in itself).

The SVG code generated by Matplotlib may contain UTF-8 characters so it is necessary to make a unicode string out of the text: unicode(figdata_svg, 'utf-8'), otherwise the HTML template will lead to an encoding exception.

We have made an alternative compute function compute_png_svg that returns both a PNG and an SVG plot:

def compute_png_svg(A, b, w, T, resolution=500):
    ...
    figfile = BytesIO()
    plt.savefig(figfile, format='svg')
    figfile.seek(0)
    figdata_svg = '<svg' + figfile.getvalue().split('<svg')[1]
    figdata_svg = unicode(figdata_svg, 'utf-8')
    return figdata_png, figdata_svg

The relevant syntax for inserting an SVG plot in the HTML template is now

{{ result[1]|safe }}

The use of safe is essential here.

Important: use safe for verbatim HTML code: Special HTML characters like <, >, &, ", and ' are escaped in a template string like {{ str }} (i.e., & is replaced by & `<` is replaced by <, etc.). We need to avoid this manipulation of the string content because result[1] contains XML code where the mentioned characters are essential part of the syntax. Writing {{str|safe}} ensures that the contents of the string str are not altered before being embedded in the HTML text.

An alternative template, view_svg.html applies the SVG plot instead of the PNG plot. We use the command-line argument svg for indicating that we want an SVG instead of a PNG plot:

# SVG or PNG plot?
svg = False
try:
    if sys.argv[1] == 'svg':
        svg = True
except IndexError:
    pass
if svg:
    from compute import compute_png_svg as compute
    template = 'view_svg.html'
else:
    from compute import compute
    template = 'view.html'

Using mpld3

The mpld3 library can convert Matplotlib plots to HTML code that can be directly embedded in a web page. Here is a basic example:

# Plot array y vs x
import matplotlib.pyplot as plt, mpld3
fig, ax = plt.subplots()
ax.plot(x, y)
html_text = mpld3.fig_to_html(fig)

The string html_text contains all the HTML code that is needed to display the plot.

The great advantage of the mpld3 library is that it contains capabilities for creating custom interactive plots through combining Matplotlib with JavaScript, see the mpld3 Example Gallery.

Plotting with the Bokeh library

As an alternative to using Matplotlib for plotting, we can utilize the Bokeh tool, which is particularly developed for graphics in web browsers. The vib3 app is similar to the previously described vib1 and vib2 app, except that we make one plot with Bokeh. Only the compute.py and view.html files are different. Obviously, we need to run Bokeh in the compute function. Normally, Bokeh stores the HTML code for the plot in a file with a specified name. We can load the text in this file and extract the relevant HTML code for a plot. However, it is easier to use Bokeh tools for returning the HTML code elements directly. The steps are exemplified in the compute.py file:

from numpy import exp, cos, linspace
import bokeh.plotting as plt
import os, re

def damped_vibrations(t, A, b, w):
    return A*exp(-b*t)*cos(w*t)

def compute(A, b, w, T, resolution=500):
    """Return filename of plot of the damped_vibration function."""
    t = linspace(0, T, resolution+1)
    u = damped_vibrations(t, A, b, w)

    # create a new plot with a title and axis labels
    TOOLS = "pan,wheel_zoom,box_zoom,reset,save,box_select,lasso_select"
    p = plt.figure(title="simple line example", tools=TOOLS,
                   x_axis_label='t', y_axis_label='y')

    # add a line renderer with legend and line thickness
    p.line(t, u, legend="u(t)", line_width=2)

    from bokeh.resources import CDN
    from bokeh.embed import components
    script, div = components(p)
    head = """
<link rel="stylesheet"
 href="http://cdn.pydata.org/bokeh/release/bokeh-0.9.0.min.css"
 type="text/css" />
<script type="text/javascript"
 src="http://cdn.pydata.org/bokeh/release/bokeh-0.9.0.min.js">
</script>
<script type="text/javascript">
Bokeh.set_log_level("info");
</script>
"""
    return head, script, div

The key data returned from compute consists of a text for loading Bokeh tools in the head part of the HTML document (common for all plots in the file) and for the plot itself there is a script tag and a div tag. The script tag can be placed anywhere, while the div tag must be placed exactly where we want to have the plot. In case of multiple plots, there will be a common script tag and one div tag for each plot.

We need to insert the three elements return from compute, available in the tuple result, into the view.html file. The link and scripts for Bokeh tools in result[0] is inserted in the head part, while the script and div tags for the plot is inserted where we want to have to plot. The complete view.html file looks like this:

<!DOCTYPE html>
<html lang="en">
   <head>
    {{ result[0]|safe }}
</head>
<body>
<script type="text/x-mathjax-config">
MathJax.Hub.Config({
  TeX: {
     equationNumbers: {  autoNumber: "AMS"  },
     extensions: ["AMSmath.js", "AMSsymbols.js", "autobold.js"]
  }
});
</script>
<script type="text/javascript" src=
"http://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML">
</script>

This web page visualizes the function \(
u(t) = Ae^{-bt}\sin (w t), \hbox{ for } t\in [0,T]
\).

<form method=post action="">
<table>
  {% for field in form %}
    <tr>
    <td class="name">\( {{ field.name }} \) &nbsp;&nbsp;</td>
    <td>{{ field(size=12) }}</td>
    <td>{{ field.label }}</td>
    {% if field.errors %}
      <td><ul class=errors>
      {% for error in field.errors %}
        <li><font color="red">{{ error }}</font></li>
      {% endfor %}</ul></td>
    {% endif %}
    </tr>
  {% endfor %}
</table>
<p><input type="submit" value="Compute"></form></p>

<p>
{% if result != None %}
<!-- script and div elements for Bokeh plot -->
{{ result[1]|safe }}
{{ result[2]|safe }}
{% endif %}
</p>
</body>
</html>

A feature of Bokeh plots is that one can zoom, pan, and save to PNG file, among other things. There is a toolbar at the top for such actions.

The controller.py file is basically the same as before (but simpler than in the vib2 app since we do not deal with PNG and/or SVG plots):

from model import InputForm
from flask import Flask, render_template, request
from compute import compute

app = Flask(__name__)

@app.route('/vib3', methods=['GET', 'POST'])
def index():
    form = InputForm(request.form)
    if request.method == 'POST' and form.validate():
        for field in form:
            # Make local variable (name field.name)
            exec('%s = %s' % (field.name, field.data))
        result = compute(A, b, w, T)
    else:
        result = None

    return render_template('view.html', form=form,
                           result=result)

if __name__ == '__main__':
    app.run(debug=True)

Finally, we remark that Bokeh plays very well with Flask. Project 9: Interactive function exploration suggests a web app that combines Bokeh with Flask in a very interactive way.

Pandas highcharts plotting library

The pandas-highcharts package is another strong alternative to Bokeh for interative plotting in web pages. It is a stable and widely used code.