Search code examples
pythonmatplotlibflaskmatplotlib-animationbytesio

Convert an animation into an in-memory file or a function


I am creating a matplotlib animation to be displayed on a flask app based on user input. The matplotlib script is similar to this:

import matplotlib.pyplot as plt
import matplotlib.animation as animation
import numpy as np

# Horizontal bar plot with gaps
fig, ax = plt.subplots()
ax.get_yaxis().set_visible(False)
ax.spines[['top', 'bottom','left','right']].set_visible(False)


y2=[20,20,20,20,20,20,20]
y3=np.array(y2)  #convert to array wont work with list
x2 =[20,15,14,13, 12,11,10]
x3=np.array(x2)
year =["2014","2015","2016","2017","2018","2019","2020"]
yr2 =np.array(year)

def animate(i):
    ax.clear()
    ax.set_ylim(16, 24)
    ax.barh(20, 60, 4 )
    ax.plot(60, 18,  marker=6, markersize=18, clip_on=False,)
    ax.annotate(r"$\bf" + str(2013) +"$" + f" ({60})", (60 , 18),xytext=(0, -25), size= 8, textcoords='offset points', ha='center', va='bottom')

    ax.barh(y3[i], x3[i], 4,color='c')
    ax.plot(x3[i], y3[i]+2, color = 'c', marker=7, markersize=18, clip_on=False,)
    ax.annotate(r"$\bf" + str(yr2[i]) +"$" + f" ({x3[i]})", (x3[i] , y3[i]+2),xytext=(0, 15), size= 8, color = 'c', textcoords='offset points', ha='center', va='bottom')

    
ani = animation.FuncAnimation(fig, animate, repeat=False,
                                    frames=len(x3),  interval=100000)

# To save the animation using Pillow as a gif
writer = animation.PillowWriter(fps=1,
                                 metadata=dict(artist='Me'),
                                 bitrate=1800)
ani.save('scatter.gif', writer=writer)

Is it possible to save gif into an in-memory file rather than saving as a gif?


Solution

    • You can save the file as a tempfile, load it into memory with io.BytesIO, and delete the file.
    • Saving the animation directly to the buffer is not possible with buf = io.BytesIO() and ani.save(buf, writer=writer), because ani.save does not accept BytesIO as the path.
    • The following code example is fully executable.
    • Tested in python 3.9.18, flask 2.2.2, matplotlib 3.7.2, numpy 1.21.5.
    from flask import Flask, Response
    import io
    import matplotlib.animation as animation
    import matplotlib.pyplot as plt
    import numpy as np
    import os
    import tempfile
    
    
    app = Flask(__name__)
    
    
    @app.route("/")
    def index():
        return """
        <html>
            <body>
                <img src="/gif" alt="animation">
            </body>
        </html>
        """
    
    
    @app.route("/gif")
    def gif():
        # Generate the GIF data and store it in a BytesIO object (buf)
        # Here you should call the function or code that generates the GIF and returns the BytesIO object.
        # For example, you can call a function that generates the matplotlib animation and returns buf.
        buf = generate_gif()  # replace this with your code that generates the GIF
    
        # Return the GIF data as a response with the correct content type
        return Response(buf.getvalue(), content_type="image/gif")
    
    
    def generate_gif():
    
        # Horizontal bar plot with gaps
        fig, ax = plt.subplots()
        ax.get_yaxis().set_visible(False)
        ax.spines[["top", "bottom", "left", "right"]].set_visible(False)
    
        y2 = [20, 20, 20, 20, 20, 20, 20]
        y3 = np.array(y2)  # convert to array won't work with list
        x2 = [20, 15, 14, 13, 12, 11, 10]
        x3 = np.array(x2)
        year = ["2014", "2015", "2016", "2017", "2018", "2019", "2020"]
        yr2 = np.array(year)
    
    
        def animate(i):
            ax.clear()
            ax.set_ylim(16, 24)
            ax.barh(20, 60, 4)
            ax.plot(60, 18, marker=6, markersize=18, clip_on=False,)
            ax.annotate(r"$\bf" + str(2013) + "$" + f" ({60})", (60, 18), xytext=(0, -25), size=8, textcoords="offset points", ha="center", va="bottom",)
    
            ax.barh(y3[i], x3[i], 4, color="c")
            ax.plot(x3[i], y3[i] + 2, color="c", marker=7, markersize=18, clip_on=False,)
            ax.annotate(r"$\bf" + str(yr2[i]) + "$" + f" ({x3[i]})", (x3[i], y3[i] + 2), xytext=(0, 15), size=8, color="c", textcoords="offset points", ha="center", va="bottom",)
    
        ani = animation.FuncAnimation(fig, animate, repeat=False, frames=len(x3), interval=100000)
    
        # Save the animation to a temporary file with a '.gif' suffix
        with tempfile.NamedTemporaryFile(delete=False, suffix=".gif") as temp_file:
            writer = animation.PillowWriter(fps=1, metadata=dict(artist="Me"), bitrate=1800)
            ani.save(temp_file.name, writer=writer)
    
            # Read the file contents into a BytesIO object
            temp_file.seek(0)
            buf = io.BytesIO(temp_file.read())
    
        # Now buf contains the gif data, and you can use buf.getvalue() to access it.
        # Don't forget to delete the temporary file
        os.remove(temp_file.name)
    
        return buf
    
    
    if __name__ == "__main__":
        app.run(debug=True)
    

    enter image description here


    Updates per comments from OP based on Given a BytesIO buffer, generate img tag in html.

    temp_file.seek(0)
    buf = io.BytesIO(temp_file.read())
    gif = base64.b64encode(buf.getbuffer()).decode("ascii")  # return gif
    
    # on the html side:
    <img src='data:image/png;base64,{{buf}}' class="responsiveImage"/>