40

I am using matplotlib to render some figure in a web app. I've used fig.savefig() before when I'm just running scripts. However, I need a function to return an actual ".png" image so that I can call it with my HTML.

Some more (possibly unnecessary) info: I am using Python Flask. I figure I could use fig.savefig() and just stick the figure in my static folder and then call it from my HTML, but I'd rather not do that every time. It would be optimal if I could just create the figure, make an image out of it, return that image, and call it from my HTML, then it goes away.

The code that creates the figure works. However, it returns a figure, which doesn't work with HTML I guess.

Here's where I call the draw_polygon in the routing, draw_polygon is the method that returns the figure:

@app.route('/images/<cropzonekey>')
def images(cropzonekey):
    fig = draw_polygons(cropzonekey)
    return render_template("images.html", title=cropzonekey, figure = fig)

And here is the HTML where I am trying to generate the image.

<html>
  <head>
    <title>{{ title }} - image</title>
  </head>
  <body>
    <img src={{ figure }} alt="Image Placeholder" height="100">
  </body>
</html>

And, as you can probably guess, when I load the page, all I get is Image Placeholder. So, they didn't like the format I fed the figure in with.

Anyone know what matplotlib methods/work-arounds turn a figure into an actual image? I am all over these docs but I can't find anything. Thanks!

BTW: didn't think it was necessary to include the python code that makes the figure, but I can include it if You guys need to see it (just didn't want to clutter the question)

  • There was some work done recently for making mpl play nice with google appEnigne, The discussions about that included examples of how do to things like this. Another option is to do it like ipython notebook, which converts the png to a string and just directly embeds that. – tacaswell Nov 21 '13 at 04:37

6 Answers6

37

You have to separate the HTML and the image into two different routes.

Your /images/<cropzonekey> route will just serve the page, and in the HTML content of that page there will be a reference to the second route, the one that serves the image.

The image is served in its own route from a memory file that you generate with savefig().

I obviously didn't test this, but I believe the following example will work as is or will get you pretty close to a working solution:

@app.route('/images/<cropzonekey>')
def images(cropzonekey):
    return render_template("images.html", title=cropzonekey)

@app.route('/fig/<cropzonekey>')
def fig(cropzonekey):
    fig = draw_polygons(cropzonekey)
    img = StringIO()
    fig.savefig(img)
    img.seek(0)
    return send_file(img, mimetype='image/png')

Your images.html template the becomes:

<html>
  <head>
    <title>{{ title }} - image</title>
  </head>
  <body>
    <img src="{{ url_for('fig', cropzonekey = title) }}" alt="Image Placeholder" height="100">
  </body>
</html>
Miguel Grinberg
  • 65,299
  • 14
  • 133
  • 152
  • 4
    Miguel, first of all, just wanna say that your flask tutorial is utterly amazing. +1 just for that. But, I'm still getting the placeholder text when I navigate to that page. Is it perhaps a problem that has nothing to do with flask (i.e. the format that is returned by `draw_polygons(cropzonekey)`? –  Nov 20 '13 at 22:16
  • After installing my changes navigate to `http://localhost:5000/fig/cropzonekey` in your browser. Do you see the image then? – Miguel Grinberg Nov 20 '13 at 22:20
  • Wow, yes it worked. Just didn't re-route to the right place, but I can fix that. Thanks so much, perfect answer! –  Nov 20 '13 at 22:24
  • @Miguel Do you see any change to integrate the matplolib web_agg backend with flask? [Here](https://github.com/matplotlib/matplotlib/blob/master/examples/user_interfaces/embedding_webagg.py) is an example that integrates it in Tornado, however I can't figure out how to integrate it in flask. – bmu Nov 21 '13 at 06:45
  • @bmu: You need web sockets for that, so the port effort is non-trivial. – Miguel Grinberg Nov 21 '13 at 07:03
  • @Miguel Thanks for the quick response. Maybe one solution (or more a workaround?) would be to use a `tornado.web.FallbackHandler` (see [this question](http://stackoverflow.com/questions/8143141/using-flask-and-tornado-together)). I think I will try this, or do you see any simpler way? – bmu Nov 21 '13 at 07:11
  • That's probably the easiest, keep the web sockets specific parts working in Tornado, but add the traditional portion in Flask. I have never tried to combine Tornado with Flask myself so I can't comment from experience, but Tornado has a [WSGI compatibility layer](http://www.tornadoweb.org/en/stable/wsgi.html) so it seems this would work just fine. – Miguel Grinberg Nov 21 '13 at 15:15
  • @MIguel, I am trying to do this but it's not working. I have a form with the action that leads to images.html. in images.html, I have my img src pointing to the "fig". But I get the error "The method is not allowed for the requested URL." I know it's because my request is "GET" and not "POST". Any advice on this? – flash Sep 17 '14 at 15:09
  • @flash, I don't really understand the problem, but if you have a form, then why don't you make it submit with a POST request? – Miguel Grinberg Sep 19 '14 at 06:04
  • @Miguel my form does submit with a post request. I think Alex is getting the same issue. He can see the image when he navigates to localhost:5000/fig/cropzonekey, because that calls the route that generates the image. But when you navigate to localhost:5000/images/cropzonekey the image isn't displayed because in the img tag source (url_for('.fig')) that route is never called for some reason. At least that's whats happening with me. I tried both url_for('fig') and url_for('.fig') – flash Sep 21 '14 at 05:20
  • @flash: are you passing the cropzonekey argument to url_for? In any case, if you inspect the tag in generated HTML you can probably figure out what the problem is. – Miguel Grinberg Sep 21 '14 at 06:59
  • @Miguel I did that, and it says the method that is being used to when the '/fig' page is being called is 'GET'. I need to make a request within the img tag to use the method 'POST'. Because my error is '405 Method not allowed". But I haven't been able to do that. – flash Sep 21 '14 at 18:19
  • 1
    Worked for me when I switched `img = StringIO()` to `img = BytesIO()` –  Aug 04 '17 at 07:01
  • Can't we approach without saving as an image? – KcH Aug 08 '19 at 12:52
8

Python 3

I went through a lot of trouble with errors like - Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'NSWindow drag regions should only be invalidated on the Main Thread!

For all those who want to use matplotlib with flask and render the graph on an html page in python 3, here you go -

In the __init__.py

import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from flask import Flask, render_template
from io import BytesIO
import base64

    @app.route('/plot')
    def plot():
        img = BytesIO()
        y = [1,2,3,4,5]
        x = [0,2,1,3,4]

        plt.plot(x,y)

        plt.savefig(img, format='png')
        plt.close()
        img.seek(0)
        plot_url = base64.b64encode(img.getvalue()).decode('utf8')

        return render_template('plot.html', plot_url=plot_url)

In flaskr/templates/plot.html

<!doctype html>
<title>heatmap - </title>
<section>
  <h2>Heatmap</h2>
  <img src="data:image/png;base64, {{ plot_url }}">
</section>

akkhil
  • 359
  • 4
  • 14
7

For Python3 ....

I have a DataFrame, I want to show this plot in Flask ....

So Create a Base64 Image of the plot.

    df_week_min_az = pd.DataFrame.from_dict(week_max_az.to_dict(),
                                            orient='index', columns=['min_az'])



    sunalt = df_week_max_angle.plot().get_figure()
    buf = io.BytesIO()
    sunalt.savefig(buf, format='png')
    buf.seek(0)
    buffer = b''.join(buf)
    b2 = base64.b64encode(buffer)
    sunalt2=b2.decode('utf-8')

I now call my template using the base64 encoded data like this....

return render_template('where.html', form=form, sunalt=sunalt2)

The relevant part of the template (i.e. the picture bit) looks like this....

 {% if sunalt != None %}

      <h2>Sun Altitude during the year</h2>
    <img src="data:image/png;base64,{{ sunalt }}">
{% endif %}

Hope that helps someone....

Tim Seed
  • 5,119
  • 2
  • 30
  • 26
3

I am working with Python 3.x, I have changed some lines of the code and it worked for me. I had the following error message: ".....object has no attribute 'savefig'"

@app.route('/fig/<cropzonekey>')

def fig(cropzonekey):
    #fig = draw_polygons(cropzonekey)
    fig = plt.plot([1,2,3,4], [1,2,3,4])
    #img = StringIO()
    img = BytesIO()
    #fig.savefig(img)
    plt.savefig(img)
    img.seek(0)
    return send_file(img, mimetype='image/png')
Rodolfo Alvarez
  • 972
  • 2
  • 10
  • 18
1
from flask import Flask, send_file
from io import StringIO
import matplotlib.pyplot as plt
from StringIO import StringIO
@app.route('/fig/')
def fig():
      plt.plot([1,2,3,4], [1,2,3,4])
      img = StringIO()
      plt.savefig(img)
      img.seek(0)
      return send_file(img, mimetype='image/png')

The other answers are correct ,I just wanted to show the header files that has to be included. This program creates a simple graph and sends it over to the html page.

1

This works pretty well for me, you can also check this URL medium blog

from flask import Flask, render_template
from PIL import Image
import base64
import io

app = Flask(__name__)

@app.route('/')
def show_image():


    # Your plt plots instructions here 
    # plt.save('generated_plot.png') 

    im = Image.open("generated_plot.png") #Open the generated image
    data = io.BytesIO() 
    im.save(data, "png")
    encoded_img_data = base64.b64encode(data.getvalue())

    return render_template("show_image.html", img=encoded_img_data.decode('utf-8'))


if __name__ == '__main__':


   app.run(debug=True)