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.

↓ click button, then..

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")

→ Clicking button downloads multiple (as applicable) page PDF of subplots
N=60 plots

N=231 plots
(Took about ten-twenty seconds..)
