Skip to content

Add a backend kwarg to savefig. #15536

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 30, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions doc/users/next_whats_new/2019-10-27-savefig-backend.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
``savefig()`` gained a ``backend`` keyword argument
---------------------------------------------------

The ``backend`` keyword argument to ``savefig`` can now be used to pick the
rendering backend without having to globally set the backend; e.g. one can save
pdfs using the pgf backend with ``savefig("file.pdf", backend="pgf")``.
46 changes: 35 additions & 11 deletions lib/matplotlib/backend_bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -1951,18 +1951,35 @@ def get_supported_filetypes_grouped(cls):
groupings[name].sort()
return groupings

def _get_output_canvas(self, fmt):
def _get_output_canvas(self, backend, fmt):
"""
Return a canvas suitable for saving figures to a specified file format.
Set the canvas in preparation for saving the figure.

If necessary, this function will switch to a registered backend that
supports the format.
"""
# Return the current canvas if it supports the requested format.
if hasattr(self, 'print_{}'.format(fmt)):
Parameters
----------
backend : str or None
If not None, switch the figure canvas to the ``FigureCanvas`` class
of the given backend.
fmt : str
If *backend* is None, then determine a suitable canvas class for
saving to format *fmt* -- either the current canvas class, if it
supports *fmt*, or whatever `get_registered_canvas_class` returns;
switch the figure canvas to that canvas class.
"""
if backend is not None:
# Return a specific canvas class, if requested.
canvas_class = (
importlib.import_module(cbook._backend_module_name(backend))
.FigureCanvas)
if not hasattr(canvas_class, f"print_{fmt}"):
raise ValueError(
f"The {backend!r} backend does not support {fmt} output")
elif hasattr(self, f"print_{fmt}"):
# Return the current canvas if it supports the requested format.
return self
# Return a default canvas for the requested format, if it exists.
canvas_class = get_registered_canvas_class(fmt)
else:
# Return a default canvas for the requested format, if it exists.
canvas_class = get_registered_canvas_class(fmt)
if canvas_class:
return self.switch_backends(canvas_class)
# Else report error for unsupported format.
Expand All @@ -1972,7 +1989,7 @@ def _get_output_canvas(self, fmt):

def print_figure(self, filename, dpi=None, facecolor=None, edgecolor=None,
orientation='portrait', format=None,
*, bbox_inches=None, **kwargs):
*, bbox_inches=None, backend=None, **kwargs):
"""
Render the figure to hardcopy. Set the figure patch face and edge
colors. This is useful because some of the GUIs have a gray figure
Expand Down Expand Up @@ -2012,6 +2029,13 @@ def print_figure(self, filename, dpi=None, facecolor=None, edgecolor=None,
A list of extra artists that will be considered when the
tight bbox is calculated.

backend : str, optional
Use a non-default backend to render the file, e.g. to render a
png file with the "cairo" backend rather than the default "agg",
or a pdf file with the "pgf" backend rather than the default
"pdf". Note that the default backend is normally sufficient. See
:ref:`the-builtin-backends` for a list of valid backends for each
file format. Custom backends can be referenced as "module://...".
"""
if format is None:
# get format from filename, or from backend's default filetype
Expand All @@ -2026,7 +2050,7 @@ def print_figure(self, filename, dpi=None, facecolor=None, edgecolor=None,
format = format.lower()

# get canvas object and print method for format
canvas = self._get_output_canvas(format)
canvas = self._get_output_canvas(backend, format)
print_method = getattr(canvas, 'print_%s' % format)

if dpi is None:
Expand Down
9 changes: 9 additions & 0 deletions lib/matplotlib/cbook/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2212,3 +2212,12 @@ def __init__(self, fget):

def __get__(self, instance, owner):
return self._fget(owner)


def _backend_module_name(name):
"""
Convert a backend name (either a standard backend -- "Agg", "TkAgg", ... --
or a custom backend -- "module://...") to the corresponding module name).
"""
return (name[9:] if name.startswith("module://")
else "matplotlib.backends.backend_{}".format(name.lower()))
8 changes: 8 additions & 0 deletions lib/matplotlib/figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -2131,6 +2131,14 @@ def savefig(self, fname, *, transparent=None, **kwargs):
A list of extra artists that will be considered when the
tight bbox is calculated.

backend : str, optional
Use a non-default backend to render the file, e.g. to render a
png file with the "cairo" backend rather than the default "agg",
or a pdf file with the "pgf" backend rather than the default
"pdf". Note that the default backend is normally sufficient. See
:ref:`the-builtin-backends` for a list of valid backends for each
file format. Custom backends can be referenced as "module://...".

metadata : dict, optional
Key/value pairs to store in the image metadata. The supported keys
and defaults depend on the image format and backend:
Expand Down
5 changes: 1 addition & 4 deletions lib/matplotlib/pyplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,10 +212,7 @@ def switch_backend(newbackend):
rcParamsOrig["backend"] = "agg"
return

backend_name = (
newbackend[9:] if newbackend.startswith("module://")
else "matplotlib.backends.backend_{}".format(newbackend.lower()))

backend_name = cbook._backend_module_name(newbackend)
backend_mod = importlib.import_module(backend_name)
Backend = type(
"Backend", (matplotlib.backend_bases._Backend,), vars(backend_mod))
Expand Down
10 changes: 10 additions & 0 deletions lib/matplotlib/tests/test_figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,16 @@ def test_savefig():
fig.savefig("fname1.png", "fname2.png")


def test_savefig_backend():
fig = plt.figure()
# Intentionally use an invalid module name.
with pytest.raises(ModuleNotFoundError, match="No module named '@absent'"):
fig.savefig("test", backend="module://@absent")
with pytest.raises(ValueError,
match="The 'pdf' backend does not support png output"):
fig.savefig("test.png", backend="pdf")


def test_figure_repr():
fig = plt.figure(figsize=(10, 20), dpi=10)
assert repr(fig) == "<Figure size 100x200 with 0 Axes>"
Expand Down
1 change: 1 addition & 0 deletions tutorials/introductory/usage.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,7 @@ def my_plotter(ax, data1, data2, param_dict):
# use a different backend. Therefore, you should avoid explicitly calling
# `~matplotlib.use` unless absolutely necessary.
#
# .. _the-builtin-backends:
#
# The builtin backends
# --------------------
Expand Down