##// END OF EJS Templates
release 8.25.0
release 8.25.0

File last commit:

r28760:1066e43a merge
r28783:40a5a37c
Show More
pylabtools.py
542 lines | 17.7 KiB | text/x-python | PythonLexer
# -*- coding: utf-8 -*-
"""Pylab (matplotlib) support utilities."""
# Copyright (c) IPython Development Team.
# Distributed under the terms of the Modified BSD License.
from io import BytesIO
from binascii import b2a_base64
from functools import partial
import warnings
from IPython.core.display import _pngxy
from IPython.utils.decorators import flag_calls
# Matplotlib backend resolution functionality moved from IPython to Matplotlib
# in IPython 8.24 and Matplotlib 3.9.0. Need to keep `backends` and `backend2gui`
# here for earlier Matplotlib and for external backend libraries such as
# mplcairo that might rely upon it.
_deprecated_backends = {
"tk": "TkAgg",
"gtk": "GTKAgg",
"gtk3": "GTK3Agg",
"gtk4": "GTK4Agg",
"wx": "WXAgg",
"qt4": "Qt4Agg",
"qt5": "Qt5Agg",
"qt6": "QtAgg",
"qt": "QtAgg",
"osx": "MacOSX",
"nbagg": "nbAgg",
"webagg": "WebAgg",
"notebook": "nbAgg",
"agg": "agg",
"svg": "svg",
"pdf": "pdf",
"ps": "ps",
"inline": "module://matplotlib_inline.backend_inline",
"ipympl": "module://ipympl.backend_nbagg",
"widget": "module://ipympl.backend_nbagg",
}
# We also need a reverse backends2guis mapping that will properly choose which
# GUI support to activate based on the desired matplotlib backend. For the
# most part it's just a reverse of the above dict, but we also need to add a
# few others that map to the same GUI manually:
_deprecated_backend2gui = dict(
zip(_deprecated_backends.values(), _deprecated_backends.keys())
)
# In the reverse mapping, there are a few extra valid matplotlib backends that
# map to the same GUI support
_deprecated_backend2gui["GTK"] = _deprecated_backend2gui["GTKCairo"] = "gtk"
_deprecated_backend2gui["GTK3Cairo"] = "gtk3"
_deprecated_backend2gui["GTK4Cairo"] = "gtk4"
_deprecated_backend2gui["WX"] = "wx"
_deprecated_backend2gui["CocoaAgg"] = "osx"
# There needs to be a hysteresis here as the new QtAgg Matplotlib backend
# supports either Qt5 or Qt6 and the IPython qt event loop support Qt4, Qt5,
# and Qt6.
_deprecated_backend2gui["QtAgg"] = "qt"
_deprecated_backend2gui["Qt4Agg"] = "qt4"
_deprecated_backend2gui["Qt5Agg"] = "qt5"
# And some backends that don't need GUI integration
del _deprecated_backend2gui["nbAgg"]
del _deprecated_backend2gui["agg"]
del _deprecated_backend2gui["svg"]
del _deprecated_backend2gui["pdf"]
del _deprecated_backend2gui["ps"]
del _deprecated_backend2gui["module://matplotlib_inline.backend_inline"]
del _deprecated_backend2gui["module://ipympl.backend_nbagg"]
# Deprecated attributes backends and backend2gui mostly following PEP 562.
def __getattr__(name):
if name in ("backends", "backend2gui"):
warnings.warn(
f"{name} is deprecated since IPython 8.24, backends are managed "
"in matplotlib and can be externally registered.",
DeprecationWarning,
)
return globals()[f"_deprecated_{name}"]
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
#-----------------------------------------------------------------------------
# Matplotlib utilities
#-----------------------------------------------------------------------------
def getfigs(*fig_nums):
"""Get a list of matplotlib figures by figure numbers.
If no arguments are given, all available figures are returned. If the
argument list contains references to invalid figures, a warning is printed
but the function continues pasting further figures.
Parameters
----------
figs : tuple
A tuple of ints giving the figure numbers of the figures to return.
"""
from matplotlib._pylab_helpers import Gcf
if not fig_nums:
fig_managers = Gcf.get_all_fig_managers()
return [fm.canvas.figure for fm in fig_managers]
else:
figs = []
for num in fig_nums:
f = Gcf.figs.get(num)
if f is None:
print('Warning: figure %s not available.' % num)
else:
figs.append(f.canvas.figure)
return figs
def figsize(sizex, sizey):
"""Set the default figure size to be [sizex, sizey].
This is just an easy to remember, convenience wrapper that sets::
matplotlib.rcParams['figure.figsize'] = [sizex, sizey]
"""
import matplotlib
matplotlib.rcParams['figure.figsize'] = [sizex, sizey]
def print_figure(fig, fmt="png", bbox_inches="tight", base64=False, **kwargs):
"""Print a figure to an image, and return the resulting file data
Returned data will be bytes unless ``fmt='svg'``,
in which case it will be unicode.
Any keyword args are passed to fig.canvas.print_figure,
such as ``quality`` or ``bbox_inches``.
If `base64` is True, return base64-encoded str instead of raw bytes
for binary-encoded image formats
.. versionadded:: 7.29
base64 argument
"""
# When there's an empty figure, we shouldn't return anything, otherwise we
# get big blank areas in the qt console.
if not fig.axes and not fig.lines:
return
dpi = fig.dpi
if fmt == 'retina':
dpi = dpi * 2
fmt = 'png'
# build keyword args
kw = {
"format":fmt,
"facecolor":fig.get_facecolor(),
"edgecolor":fig.get_edgecolor(),
"dpi":dpi,
"bbox_inches":bbox_inches,
}
# **kwargs get higher priority
kw.update(kwargs)
bytes_io = BytesIO()
if fig.canvas is None:
from matplotlib.backend_bases import FigureCanvasBase
FigureCanvasBase(fig)
fig.canvas.print_figure(bytes_io, **kw)
data = bytes_io.getvalue()
if fmt == 'svg':
data = data.decode('utf-8')
elif base64:
data = b2a_base64(data, newline=False).decode("ascii")
return data
def retina_figure(fig, base64=False, **kwargs):
"""format a figure as a pixel-doubled (retina) PNG
If `base64` is True, return base64-encoded str instead of raw bytes
for binary-encoded image formats
.. versionadded:: 7.29
base64 argument
"""
pngdata = print_figure(fig, fmt="retina", base64=False, **kwargs)
# Make sure that retina_figure acts just like print_figure and returns
# None when the figure is empty.
if pngdata is None:
return
w, h = _pngxy(pngdata)
metadata = {"width": w//2, "height":h//2}
if base64:
pngdata = b2a_base64(pngdata, newline=False).decode("ascii")
return pngdata, metadata
# We need a little factory function here to create the closure where
# safe_execfile can live.
def mpl_runner(safe_execfile):
"""Factory to return a matplotlib-enabled runner for %run.
Parameters
----------
safe_execfile : function
This must be a function with the same interface as the
:meth:`safe_execfile` method of IPython.
Returns
-------
A function suitable for use as the ``runner`` argument of the %run magic
function.
"""
def mpl_execfile(fname,*where,**kw):
"""matplotlib-aware wrapper around safe_execfile.
Its interface is identical to that of the :func:`execfile` builtin.
This is ultimately a call to execfile(), but wrapped in safeties to
properly handle interactive rendering."""
import matplotlib
import matplotlib.pyplot as plt
# print('*** Matplotlib runner ***') # dbg
# turn off rendering until end of script
with matplotlib.rc_context({"interactive": False}):
safe_execfile(fname, *where, **kw)
if matplotlib.is_interactive():
plt.show()
# make rendering call now, if the user tried to do it
if plt.draw_if_interactive.called:
plt.draw()
plt.draw_if_interactive.called = False
# re-draw everything that is stale
try:
da = plt.draw_all
except AttributeError:
pass
else:
da()
return mpl_execfile
def _reshow_nbagg_figure(fig):
"""reshow an nbagg figure"""
try:
reshow = fig.canvas.manager.reshow
except AttributeError as e:
raise NotImplementedError() from e
else:
reshow()
def select_figure_formats(shell, formats, **kwargs):
"""Select figure formats for the inline backend.
Parameters
----------
shell : InteractiveShell
The main IPython instance.
formats : str or set
One or a set of figure formats to enable: 'png', 'retina', 'jpeg', 'svg', 'pdf'.
**kwargs : any
Extra keyword arguments to be passed to fig.canvas.print_figure.
"""
import matplotlib
from matplotlib.figure import Figure
svg_formatter = shell.display_formatter.formatters['image/svg+xml']
png_formatter = shell.display_formatter.formatters['image/png']
jpg_formatter = shell.display_formatter.formatters['image/jpeg']
pdf_formatter = shell.display_formatter.formatters['application/pdf']
if isinstance(formats, str):
formats = {formats}
# cast in case of list / tuple
formats = set(formats)
[ f.pop(Figure, None) for f in shell.display_formatter.formatters.values() ]
mplbackend = matplotlib.get_backend().lower()
if mplbackend in ("nbagg", "ipympl", "widget", "module://ipympl.backend_nbagg"):
formatter = shell.display_formatter.ipython_display_formatter
formatter.for_type(Figure, _reshow_nbagg_figure)
supported = {'png', 'png2x', 'retina', 'jpg', 'jpeg', 'svg', 'pdf'}
bad = formats.difference(supported)
if bad:
bs = "%s" % ','.join([repr(f) for f in bad])
gs = "%s" % ','.join([repr(f) for f in supported])
raise ValueError("supported formats are: %s not %s" % (gs, bs))
if "png" in formats:
png_formatter.for_type(
Figure, partial(print_figure, fmt="png", base64=True, **kwargs)
)
if "retina" in formats or "png2x" in formats:
png_formatter.for_type(Figure, partial(retina_figure, base64=True, **kwargs))
if "jpg" in formats or "jpeg" in formats:
jpg_formatter.for_type(
Figure, partial(print_figure, fmt="jpg", base64=True, **kwargs)
)
if "svg" in formats:
svg_formatter.for_type(Figure, partial(print_figure, fmt="svg", **kwargs))
if "pdf" in formats:
pdf_formatter.for_type(
Figure, partial(print_figure, fmt="pdf", base64=True, **kwargs)
)
#-----------------------------------------------------------------------------
# Code for initializing matplotlib and importing pylab
#-----------------------------------------------------------------------------
def find_gui_and_backend(gui=None, gui_select=None):
"""Given a gui string return the gui and mpl backend.
Parameters
----------
gui : str
Can be one of ('tk','gtk','wx','qt','qt4','inline','agg').
gui_select : str
Can be one of ('tk','gtk','wx','qt','qt4','inline').
This is any gui already selected by the shell.
Returns
-------
A tuple of (gui, backend) where backend is one of ('TkAgg','GTKAgg',
'WXAgg','Qt4Agg','module://matplotlib_inline.backend_inline','agg').
"""
import matplotlib
if _matplotlib_manages_backends():
backend_registry = matplotlib.backends.registry.backend_registry
# gui argument may be a gui event loop or may be a backend name.
if gui in ("auto", None):
backend = matplotlib.rcParamsOrig["backend"]
backend, gui = backend_registry.resolve_backend(backend)
else:
gui = _convert_gui_to_matplotlib(gui)
backend, gui = backend_registry.resolve_gui_or_backend(gui)
gui = _convert_gui_from_matplotlib(gui)
return gui, backend
# Fallback to previous behaviour (Matplotlib < 3.9)
mpl_version_info = getattr(matplotlib, "__version_info__", (0, 0))
has_unified_qt_backend = mpl_version_info >= (3, 5)
from IPython.core.pylabtools import backends
backends_ = dict(backends)
if not has_unified_qt_backend:
backends_["qt"] = "qt5agg"
if gui and gui != 'auto':
# select backend based on requested gui
backend = backends_[gui]
if gui == 'agg':
gui = None
else:
# We need to read the backend from the original data structure, *not*
# from mpl.rcParams, since a prior invocation of %matplotlib may have
# overwritten that.
# WARNING: this assumes matplotlib 1.1 or newer!!
backend = matplotlib.rcParamsOrig['backend']
# In this case, we need to find what the appropriate gui selection call
# should be for IPython, so we can activate inputhook accordingly
from IPython.core.pylabtools import backend2gui
gui = backend2gui.get(backend, None)
# If we have already had a gui active, we need it and inline are the
# ones allowed.
if gui_select and gui != gui_select:
gui = gui_select
backend = backends_[gui]
# Matplotlib before _matplotlib_manages_backends() can return "inline" for
# no gui event loop rather than the None that IPython >= 8.24.0 expects.
if gui == "inline":
gui = None
return gui, backend
def activate_matplotlib(backend):
"""Activate the given backend and set interactive to True."""
import matplotlib
matplotlib.interactive(True)
# Matplotlib had a bug where even switch_backend could not force
# the rcParam to update. This needs to be set *before* the module
# magic of switch_backend().
matplotlib.rcParams['backend'] = backend
# Due to circular imports, pyplot may be only partially initialised
# when this function runs.
# So avoid needing matplotlib attribute-lookup to access pyplot.
from matplotlib import pyplot as plt
plt.switch_backend(backend)
plt.show._needmain = False
# We need to detect at runtime whether show() is called by the user.
# For this, we wrap it into a decorator which adds a 'called' flag.
plt.draw_if_interactive = flag_calls(plt.draw_if_interactive)
def import_pylab(user_ns, import_all=True):
"""Populate the namespace with pylab-related values.
Imports matplotlib, pylab, numpy, and everything from pylab and numpy.
Also imports a few names from IPython (figsize, display, getfigs)
"""
# Import numpy as np/pyplot as plt are conventions we're trying to
# somewhat standardize on. Making them available to users by default
# will greatly help this.
s = ("import numpy\n"
"import matplotlib\n"
"from matplotlib import pylab, mlab, pyplot\n"
"np = numpy\n"
"plt = pyplot\n"
)
exec(s, user_ns)
if import_all:
s = ("from matplotlib.pylab import *\n"
"from numpy import *\n")
exec(s, user_ns)
# IPython symbols to add
user_ns['figsize'] = figsize
from IPython.display import display
# Add display and getfigs to the user's namespace
user_ns['display'] = display
user_ns['getfigs'] = getfigs
def configure_inline_support(shell, backend):
"""
.. deprecated:: 7.23
use `matplotlib_inline.backend_inline.configure_inline_support()`
Configure an IPython shell object for matplotlib use.
Parameters
----------
shell : InteractiveShell instance
backend : matplotlib backend
"""
warnings.warn(
"`configure_inline_support` is deprecated since IPython 7.23, directly "
"use `matplotlib_inline.backend_inline.configure_inline_support()`",
DeprecationWarning,
stacklevel=2,
)
from matplotlib_inline.backend_inline import (
configure_inline_support as configure_inline_support_orig,
)
configure_inline_support_orig(shell, backend)
# Determine if Matplotlib manages backends only if needed, and cache result.
# Do not read this directly, instead use _matplotlib_manages_backends().
_matplotlib_manages_backends_value: bool | None = None
def _matplotlib_manages_backends() -> bool:
"""Return True if Matplotlib manages backends, False otherwise.
If it returns True, the caller can be sure that
matplotlib.backends.registry.backend_registry is available along with
member functions resolve_gui_or_backend, resolve_backend, list_all, and
list_gui_frameworks.
This function can be removed as it will always return True when Python
3.12, the latest version supported by Matplotlib < 3.9, reaches
end-of-life in late 2028.
"""
global _matplotlib_manages_backends_value
if _matplotlib_manages_backends_value is None:
try:
from matplotlib.backends.registry import backend_registry
_matplotlib_manages_backends_value = hasattr(
backend_registry, "resolve_gui_or_backend"
)
except ImportError:
_matplotlib_manages_backends_value = False
return _matplotlib_manages_backends_value
def _list_matplotlib_backends_and_gui_loops() -> list[str]:
"""Return list of all Matplotlib backends and GUI event loops.
This is the list returned by
%matplotlib --list
"""
if _matplotlib_manages_backends():
from matplotlib.backends.registry import backend_registry
ret = backend_registry.list_all() + [
_convert_gui_from_matplotlib(gui)
for gui in backend_registry.list_gui_frameworks()
]
else:
from IPython.core import pylabtools
ret = list(pylabtools.backends.keys())
return sorted(["auto"] + ret)
# Matplotlib and IPython do not always use the same gui framework name.
# Always use the approprate one of these conversion functions when passing a
# gui framework name to/from Matplotlib.
def _convert_gui_to_matplotlib(gui: str | None) -> str | None:
if gui and gui.lower() == "osx":
return "macosx"
return gui
def _convert_gui_from_matplotlib(gui: str | None) -> str | None:
if gui and gui.lower() == "macosx":
return "osx"
return gui