1

I've a Dash app running which calls functions which create and save charts.
The app runs into timeouts which are caused from the drawing of the charts. Matplotlib prints the warning:
Starting a Matplotlib GUI outside of the main thread will likely fail.

From my unterstanding the Dash app is hosted by Flask which runs different threads. This seems to be a problem for matploblib since it's not thread save. I run the app with the threaded=False parameter but the problem still exists. When debugging the app it seems like Flask is still running multiple threads.

The proposed solution from the matplotlib website is not to use pyplot but instead use the OOP approach.

Does someone know another solution to solve the problem?

Example code:

import dash
from dash import html
from dash.dependencies import Output, Input
import matplotlib.pyplot as plt

app = dash.Dash(__name__)
app.layout = html.Div([html.Button("Button", id="btn"), html.Div(id="placeholder")])


def draw_figure():
    fig, ax = plt.subplots(1, 1)


@app.callback(
    Output("placeholder", "children"),
    Input("btn", "n_clicks"),
    prevent_initial_call=True,
)
def func(_):
    for _ in range(20):
        draw_figure()
    return ""


if __name__ == "__main__":
    app.run_server(debug=True, threaded=False)
davidism
  • 121,510
  • 29
  • 395
  • 339
j. DOE
  • 238
  • 1
  • 2
  • 15
  • you should definitely use the OOP interface. But you also need to share at least some of your code, ideally the absolute minimum required to reproduce the issue you're seeing (which may be tricky in your case, but something is better than nothing) – Paul H Oct 20 '21 at 17:00
  • @PaulH, you are right about the code example. I added one. It reproduces the issue. The code doesn't run in a timeout but at least it produces the the warnings. – j. DOE Oct 20 '21 at 17:09

1 Answers1

2

It's not too bad switching from pyplot and, thankfully, there is an immensely helpful new component in Dash's dcc library just for this (dcc.Download)! (Previously, you had to use flask and werkzeug and use the Url dash component and it could be a pretty horrible time-sink; it's much simpler now)

Basic Example

import os
from time import time_ns

import dash
import matplotlib
import matplotlib as mpl

matplotlib.use("agg")

import numpy as np

from dash import dcc
from dash import html
from dash.dependencies import Input
from dash.dependencies import Output
from matplotlib.backends.backend_agg import FigureCanvasAgg
from matplotlib.figure import Figure


app = dash.Dash(__name__)

app.layout = html.Div(
    [
        html.Button(
            "Generate plot",
            id="generate-plot",
            style={
                "margin": "10% 40% 10% 40%",
                "width": "20%",
                "fontSize": "1.1rem",
            },
        ),
        dcc.Download(id="download-image"),
    ]
)


def draw_figure():
    fig = Figure()
    ax = fig.add_subplot(111)

    # Random dist plot
    scaled_y = np.random.randint(20, 30)
    random_data = np.random.poisson(scaled_y, 100)
    ax.hist(random_data, bins=12, fc=(0, 0, 0, 0), lw=0.75, ec="b")

    # Axes label properties
    ax.set_title("Figure Title", size=26)
    ax.set_xlabel("X Label", size=14)
    ax.set_ylabel("Y Label", size=14)

    # NOTE:
    # Save figure ~
    # * BUT DO NOT USE PYLAB *
    #   Write figure to output file (png|pdf).

    # Make the PNG
    canvas = FigureCanvasAgg(fig)
    # The size * the dpi gives the final image size
    #   a4"x4" image * 80 dpi ==> 320x320 pixel image
    fig_path = f"rand-poiss-hist_{time_ns()}.png"
    canvas.print_figure(fig_path, dpi=150, bbox_inches="tight")
    return fig_path


@app.callback(
    Output("download-image", "data"),
    Input("generate-plot", "n_clicks"),
    prevent_initial_call=True,
)
def generate_downloadable_figure(n_clicks):
    if n_clicks > 1:
        fig_path = draw_figure()
        return dcc.send_file(fig_path)


if __name__ == "__main__":
    app.run_server(debug=True, dev_tools_hot_reload=True, host="0.0.0.0")

I generate a random plot, just for demonstration purposes, but hopefully this is enough to make it clear what you need to do. The trick is that you cannot use pyplot, but have to use the "agg" mpl backend and use what's called a FigureCanvasAgg. The plot won't show up (although you could code that additionally, if you wanted to), it just downloads when clicking the button.

base app

↓ click button, then..

after clicking button example downloaded plot

Generating & Downloading High-Throughput Quantities of Subplots in Dash (without any use of mpl.pyplot)

In this extended example of the previous code, I added a numeric input component (dcc.Input w/ type='number', max=1200, step=1; you can of course also just type any number 1 <= n <= 1200), so when you click the download button, the file you get is a pdf with potentially hundreds, or thousands, of plots having been generated.

import os
import random
from time import time_ns

import dash
import matplotlib
import matplotlib as mpl

matplotlib.use("agg")

import numpy as np

from dash import dcc
from dash import html
from dash.dependencies import Input
from dash.dependencies import Output
from dash.dependencies import State
from matplotlib.backends.backend_pdf import PdfPages
from matplotlib.figure import Figure

import seaborn as sns

sns.set(
    font_scale=0.2
)  # this erases labels for any blank plots on the last page

ctheme = [
    "k", "gray", "magenta", "fuchsia", "#be03fd", "#1e488f",
    (0.443_137_254_901_960_76, 0.443_137_254_901_960_76,
    0.886_274_509_803_921_53, ), "#75bbfd", "teal", "lime", "g", 
    (0.666_667_4, 0.666_666_3, 0.290_780_141_843_971_38), "y", 
    "#f1da7a", "tan", "orange", "maroon", "r"
] # colors to blend to any scalar-spread palette form


def new_page(m, n):

    fig = Figure()
    axarr = fig.subplots(m, n, sharex="all", sharey="all") 
    arr_ij = [(x, y) for x, y in np.ndindex(m, n)]
    subplots = [axarr[index] for index in arr_ij]

    return (fig, subplots)


def generate_figures(n_plots, m=6, n=5):

    fig_path = f"rand-poiss-hist_N={n_plots}_{time_ns()}.pdf"

    colors = sns.blend_palette(ctheme, n_plots)

    x = 0

    with PdfPages(fig_path) as pdf:

        for _ in range((n_plots // (m * n)) + 1):

            fig, subplots = new_page(m, n)
            fig.subplots_adjust(wspace=0.5, hspace=0.5)

            for i in range(m * n):  # Random dist plots
                ax = subplots[i]
                x += 1
                if x <= n_plots:


                    scaled_y = np.random.randint(20, 30)
                    random_data = np.random.poisson(scaled_y, 100)
                    ax.hist(
                        random_data,
                        bins=12,
                        fc=(0, 0, 0, 0),
                        lw=0.75,
                        ec=colors.pop(),
                    )

                    # Axes label properties
                    ax.set_title(f"fig.{x}", size=6)
                    if ax.is_last_row() or ((n_plots - x) <= n):
                        ax.set_xlabel("X Label", size=4)
                    if ax.is_first_col():
                        ax.set_ylabel("Y Label", size=4)
                    # ax.set_xmargin(2)
                    # ax.set_ymargin(2)

            # NOTE:
            # Save figure ~
            # * BUT DO NOT USE PYLAB *
            #   Write figure to output file (png|pdf).
            pdf.savefig(fig)

    return fig_path


app = dash.Dash(__name__)

app.layout = html.Div(
    [
        html.Button(
            "Generate plots",
            id="generate-plot",
            style={
                "width": "30%",
                "fontSize": "1.1rem",
            },
        ),
        html.Br(),
        html.Code("Enter number of plots to generate:"),
        html.Br(),
        dcc.Input(id="range", type="number", min=1, max=1200, step=1),
        dcc.Download(id="download-image"),
    ],
    style={"margin": "10% 40% 10% 40%"}
)


@app.callback(
    Output("download-image", "data"),
    Input("generate-plot", "n_clicks"),
    State("range", "value"),
    prevent_initial_call=True,
)
def generate_downloadable_figure(n_clicks, n_plots):
    if n_clicks > 0:
        fig_path = generate_figures(n_plots)
        return dcc.send_file(fig_path)


if __name__ == "__main__":
    app.run_server(debug=True, dev_tools_hot_reload=True, host="0.0.0.0")

multi-plot app layout

→ Clicking button downloads multiple (as applicable) page PDF of subplots

N=60 plots

N=60 plots

N=231 plots

(Took about ten-twenty seconds..)

N=231 plots

John Collins
  • 2,067
  • 9
  • 17
  • And since you are, you say, generating many plots, I would recommend (I wonder if this would work), using mpl's PdfPages and save them via appending as subplots to pages of a multi-page PDF. Then the download button would download the pdf as a single file containing the many plots. I have an old post actually on making those which may be helpful (of course it'd have to be amended to use the canvas obj instead of pyplot, but I think it may still work just fine): https://stackoverflow.com/questions/38938454/python-saving-multiple-subplot-figures-to-pdf/38943527#38943527 – John Collins Oct 20 '21 at 22:26
  • Yeah just checked the docs, I may update my answer so that the downloaded file is a pdf with, idk, say hundreds of subplots generated within seconds and you don't have to worry about threads. Because even though the mpl backend has changed, we're still using the same `Figure` object which would allow for creating subplots exactly as I do in that answer link I provided in prev comment. Does this address your concern? How to generate load of plots in a dash app and then download them? – John Collins Oct 20 '21 at 22:35
  • This solves the problem! Thanks – j. DOE Oct 21 '21 at 13:35