How to display a Matplotlib chart with FastAPI/ Nextjs without saving chart locally?

Question:

I am using Nextjs frontend and FastAPI backend for a website. I have an input form for an ‘ethereum address’ on the frontend and using the inputted address, I am generating a matplotlib chart in the backend that displays ‘ethereum balance over time’. Now, I am trying to return this chart using FastAPI so I can display it on the frontend. I do not want to save the chart locally.

Here is my relevant code so far:

Frontend/ nexjs file called ‘Chart.tsx’. ‘ethAddress’ in the body is capturing data inputted in the input form.

fetch("http://localhost:8000/image", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(ethAddress),
    }).then(fetchEthAddresses);

Backend python file that generates matplotlib chart called ethBalanceTracker.py

#Imports
#Logic for chart here

        plt.plot(times, balances)
        buf = BytesIO()
        plt.savefig(buf, format="png")
        buf.seek(0)

        return StreamingResponse(buf, media_type="image/png")

Backend python file using FastAPI called api.py

@app.get("/image")
async def get_images() -> dict:
    return {"data": images}

@app.post("/image")
async def add_image(ethAddress: dict) -> dict:

    test = EthBalanceTracker.get_transactions(ethAddress["ethAddress"])
    images.append(test)

I have tried the above code and a few other variants. I am using StreamingResponse because I do not want to save the chart locally. My issue is I cannot get the chart to display in localhost:8000/images and am getting an 'Internal Server Error'.

Asked By: spal

||

Answers:

You should pass buf.getvalue() as the content of your Response, in order to get the bytes containing the entire contents of the buffer. Additionally, you shouldn’t be using StreamingResponse, if the entire image data are already loaded into memory (in this case, in an in-memory bytes buffer), but rather return a Response directly and set the Content-Disposition header, so that the image can be viewed in the browser, as described in this answer, as well as this and this answer. If you are using Fetch API, or Axios, to fetch the image, please have a look at this answer on how to display the image on client side. You could also use FastAPI/Starlette’s BackgroundTasks to close the buffer after returning the response, in order to release the memory, as described here. Example:

import io
import matplotlib
matplotlib.use('AGG')
import matplotlib.pyplot as plt
from fastapi import FastAPI, Response, BackgroundTasks

app = FastAPI()

def create_img():
    plt.rcParams['figure.figsize'] = [7.50, 3.50]
    plt.rcParams['figure.autolayout'] = True
    fig = plt.figure()  # make sure to call this, in order to create a new figure
    plt.plot([1, 2])
    img_buf = io.BytesIO()
    plt.savefig(img_buf, format='png')
    plt.close()
    return img_buf
    
@app.get('/')
def get_img(background_tasks: BackgroundTasks):
    img_buf = create_img()
    background_tasks.add_task(img_buf.close)
    headers = {'Content-Disposition': 'inline; filename="out.png"'}
    return Response(img_buf.getvalue(), headers=headers, media_type='image/png')

If you would like get_img() being an async def endpoint, you could then execute the synchronous create_img() function in an external ThreadPool or PorcessPool so that the event loop does not get blocked. Please have a look at this answer, which provides details and examples on how to do that.

On a side note, if you are gettting the following warning when using matplotlib:

UserWarning: Starting a Matplotlib GUI outside of the main thread will likely fail.
WARNING: QApplication was not created in the main() thread.

this is because matplotlib is not thread-safe, and most GUI backends require being run from the main thread (this is actually a warning from Qt library itself). To avoid getting that warning, you can simply cahnge to a non-GUI backend—since you don’t even need one, as you are displaying the image in the user’s browser on client side—using matplotlib.use(), as shown in the Backends documentation and in the example above (Note: matplotlib.use() must be used before importing pyplot). The AGG used in the example above is a backend that renders graphs as PNGs.

Answered By: Chris

The answer above by Chris works quite well (thank you!) but I found in my own work that adding an async can be important. Without an async, the buffers were being cut short when multiple requests were entering the FastAPI server. The solution was to allow asynchronous processing. Modifying Chris’s answer:

import io
import matplotlib
matplotlib.use('AGG')
import matplotlib.pyplot as plt
from fastapi import FastAPI, Response, BackgroundTasks

app = FastAPI()

def create_img():
    plt.rcParams['figure.figsize'] = [7.50, 3.50]
    plt.rcParams['figure.autolayout'] = True
    plt.plot([1, 2])
    img_buf = io.BytesIO()
    plt.savefig(img_buf, format='png')
    plt.close()
    return img_buf
    
@app.get('/')
async def get_img(background_tasks: BackgroundTasks):
    img_buf = create_img()
    # get the entire buffer content
    # because of the async, this will await the loading of all content
    bufContents: bytes = img_buf.getvalue()
    background_tasks.add_task(img_buf.close)
    headers = {'Content-Disposition': 'inline; filename="out.png"'}
    return Response(bufContents, headers=headers, media_type='image/png')
Answered By: Gilad
Categories: questions Tags: , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.