pylabtools.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542
  1. # -*- coding: utf-8 -*-
  2. """Pylab (matplotlib) support utilities."""
  3. # Copyright (c) IPython Development Team.
  4. # Distributed under the terms of the Modified BSD License.
  5. from io import BytesIO
  6. from binascii import b2a_base64
  7. from functools import partial
  8. import warnings
  9. from IPython.core.display import _pngxy
  10. from IPython.utils.decorators import flag_calls
  11. # Matplotlib backend resolution functionality moved from IPython to Matplotlib
  12. # in IPython 8.24 and Matplotlib 3.9.0. Need to keep `backends` and `backend2gui`
  13. # here for earlier Matplotlib and for external backend libraries such as
  14. # mplcairo that might rely upon it.
  15. _deprecated_backends = {
  16. "tk": "TkAgg",
  17. "gtk": "GTKAgg",
  18. "gtk3": "GTK3Agg",
  19. "gtk4": "GTK4Agg",
  20. "wx": "WXAgg",
  21. "qt4": "Qt4Agg",
  22. "qt5": "Qt5Agg",
  23. "qt6": "QtAgg",
  24. "qt": "QtAgg",
  25. "osx": "MacOSX",
  26. "nbagg": "nbAgg",
  27. "webagg": "WebAgg",
  28. "notebook": "nbAgg",
  29. "agg": "agg",
  30. "svg": "svg",
  31. "pdf": "pdf",
  32. "ps": "ps",
  33. "inline": "module://matplotlib_inline.backend_inline",
  34. "ipympl": "module://ipympl.backend_nbagg",
  35. "widget": "module://ipympl.backend_nbagg",
  36. }
  37. # We also need a reverse backends2guis mapping that will properly choose which
  38. # GUI support to activate based on the desired matplotlib backend. For the
  39. # most part it's just a reverse of the above dict, but we also need to add a
  40. # few others that map to the same GUI manually:
  41. _deprecated_backend2gui = dict(
  42. zip(_deprecated_backends.values(), _deprecated_backends.keys())
  43. )
  44. # In the reverse mapping, there are a few extra valid matplotlib backends that
  45. # map to the same GUI support
  46. _deprecated_backend2gui["GTK"] = _deprecated_backend2gui["GTKCairo"] = "gtk"
  47. _deprecated_backend2gui["GTK3Cairo"] = "gtk3"
  48. _deprecated_backend2gui["GTK4Cairo"] = "gtk4"
  49. _deprecated_backend2gui["WX"] = "wx"
  50. _deprecated_backend2gui["CocoaAgg"] = "osx"
  51. # There needs to be a hysteresis here as the new QtAgg Matplotlib backend
  52. # supports either Qt5 or Qt6 and the IPython qt event loop support Qt4, Qt5,
  53. # and Qt6.
  54. _deprecated_backend2gui["QtAgg"] = "qt"
  55. _deprecated_backend2gui["Qt4Agg"] = "qt4"
  56. _deprecated_backend2gui["Qt5Agg"] = "qt5"
  57. # And some backends that don't need GUI integration
  58. del _deprecated_backend2gui["nbAgg"]
  59. del _deprecated_backend2gui["agg"]
  60. del _deprecated_backend2gui["svg"]
  61. del _deprecated_backend2gui["pdf"]
  62. del _deprecated_backend2gui["ps"]
  63. del _deprecated_backend2gui["module://matplotlib_inline.backend_inline"]
  64. del _deprecated_backend2gui["module://ipympl.backend_nbagg"]
  65. # Deprecated attributes backends and backend2gui mostly following PEP 562.
  66. def __getattr__(name):
  67. if name in ("backends", "backend2gui"):
  68. warnings.warn(
  69. f"{name} is deprecated since IPython 8.24, backends are managed "
  70. "in matplotlib and can be externally registered.",
  71. DeprecationWarning,
  72. )
  73. return globals()[f"_deprecated_{name}"]
  74. raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
  75. #-----------------------------------------------------------------------------
  76. # Matplotlib utilities
  77. #-----------------------------------------------------------------------------
  78. def getfigs(*fig_nums):
  79. """Get a list of matplotlib figures by figure numbers.
  80. If no arguments are given, all available figures are returned. If the
  81. argument list contains references to invalid figures, a warning is printed
  82. but the function continues pasting further figures.
  83. Parameters
  84. ----------
  85. figs : tuple
  86. A tuple of ints giving the figure numbers of the figures to return.
  87. """
  88. from matplotlib._pylab_helpers import Gcf
  89. if not fig_nums:
  90. fig_managers = Gcf.get_all_fig_managers()
  91. return [fm.canvas.figure for fm in fig_managers]
  92. else:
  93. figs = []
  94. for num in fig_nums:
  95. f = Gcf.figs.get(num)
  96. if f is None:
  97. print('Warning: figure %s not available.' % num)
  98. else:
  99. figs.append(f.canvas.figure)
  100. return figs
  101. def figsize(sizex, sizey):
  102. """Set the default figure size to be [sizex, sizey].
  103. This is just an easy to remember, convenience wrapper that sets::
  104. matplotlib.rcParams['figure.figsize'] = [sizex, sizey]
  105. """
  106. import matplotlib
  107. matplotlib.rcParams['figure.figsize'] = [sizex, sizey]
  108. def print_figure(fig, fmt="png", bbox_inches="tight", base64=False, **kwargs):
  109. """Print a figure to an image, and return the resulting file data
  110. Returned data will be bytes unless ``fmt='svg'``,
  111. in which case it will be unicode.
  112. Any keyword args are passed to fig.canvas.print_figure,
  113. such as ``quality`` or ``bbox_inches``.
  114. If `base64` is True, return base64-encoded str instead of raw bytes
  115. for binary-encoded image formats
  116. .. versionadded:: 7.29
  117. base64 argument
  118. """
  119. # When there's an empty figure, we shouldn't return anything, otherwise we
  120. # get big blank areas in the qt console.
  121. if not fig.axes and not fig.lines:
  122. return
  123. dpi = fig.dpi
  124. if fmt == 'retina':
  125. dpi = dpi * 2
  126. fmt = 'png'
  127. # build keyword args
  128. kw = {
  129. "format":fmt,
  130. "facecolor":fig.get_facecolor(),
  131. "edgecolor":fig.get_edgecolor(),
  132. "dpi":dpi,
  133. "bbox_inches":bbox_inches,
  134. }
  135. # **kwargs get higher priority
  136. kw.update(kwargs)
  137. bytes_io = BytesIO()
  138. if fig.canvas is None:
  139. from matplotlib.backend_bases import FigureCanvasBase
  140. FigureCanvasBase(fig)
  141. fig.canvas.print_figure(bytes_io, **kw)
  142. data = bytes_io.getvalue()
  143. if fmt == 'svg':
  144. data = data.decode('utf-8')
  145. elif base64:
  146. data = b2a_base64(data, newline=False).decode("ascii")
  147. return data
  148. def retina_figure(fig, base64=False, **kwargs):
  149. """format a figure as a pixel-doubled (retina) PNG
  150. If `base64` is True, return base64-encoded str instead of raw bytes
  151. for binary-encoded image formats
  152. .. versionadded:: 7.29
  153. base64 argument
  154. """
  155. pngdata = print_figure(fig, fmt="retina", base64=False, **kwargs)
  156. # Make sure that retina_figure acts just like print_figure and returns
  157. # None when the figure is empty.
  158. if pngdata is None:
  159. return
  160. w, h = _pngxy(pngdata)
  161. metadata = {"width": w//2, "height":h//2}
  162. if base64:
  163. pngdata = b2a_base64(pngdata, newline=False).decode("ascii")
  164. return pngdata, metadata
  165. # We need a little factory function here to create the closure where
  166. # safe_execfile can live.
  167. def mpl_runner(safe_execfile):
  168. """Factory to return a matplotlib-enabled runner for %run.
  169. Parameters
  170. ----------
  171. safe_execfile : function
  172. This must be a function with the same interface as the
  173. :meth:`safe_execfile` method of IPython.
  174. Returns
  175. -------
  176. A function suitable for use as the ``runner`` argument of the %run magic
  177. function.
  178. """
  179. def mpl_execfile(fname,*where,**kw):
  180. """matplotlib-aware wrapper around safe_execfile.
  181. Its interface is identical to that of the :func:`execfile` builtin.
  182. This is ultimately a call to execfile(), but wrapped in safeties to
  183. properly handle interactive rendering."""
  184. import matplotlib
  185. import matplotlib.pyplot as plt
  186. # print('*** Matplotlib runner ***') # dbg
  187. # turn off rendering until end of script
  188. with matplotlib.rc_context({"interactive": False}):
  189. safe_execfile(fname, *where, **kw)
  190. if matplotlib.is_interactive():
  191. plt.show()
  192. # make rendering call now, if the user tried to do it
  193. if plt.draw_if_interactive.called:
  194. plt.draw()
  195. plt.draw_if_interactive.called = False
  196. # re-draw everything that is stale
  197. try:
  198. da = plt.draw_all
  199. except AttributeError:
  200. pass
  201. else:
  202. da()
  203. return mpl_execfile
  204. def _reshow_nbagg_figure(fig):
  205. """reshow an nbagg figure"""
  206. try:
  207. reshow = fig.canvas.manager.reshow
  208. except AttributeError as e:
  209. raise NotImplementedError() from e
  210. else:
  211. reshow()
  212. def select_figure_formats(shell, formats, **kwargs):
  213. """Select figure formats for the inline backend.
  214. Parameters
  215. ----------
  216. shell : InteractiveShell
  217. The main IPython instance.
  218. formats : str or set
  219. One or a set of figure formats to enable: 'png', 'retina', 'jpeg', 'svg', 'pdf'.
  220. **kwargs : any
  221. Extra keyword arguments to be passed to fig.canvas.print_figure.
  222. """
  223. import matplotlib
  224. from matplotlib.figure import Figure
  225. svg_formatter = shell.display_formatter.formatters['image/svg+xml']
  226. png_formatter = shell.display_formatter.formatters['image/png']
  227. jpg_formatter = shell.display_formatter.formatters['image/jpeg']
  228. pdf_formatter = shell.display_formatter.formatters['application/pdf']
  229. if isinstance(formats, str):
  230. formats = {formats}
  231. # cast in case of list / tuple
  232. formats = set(formats)
  233. [ f.pop(Figure, None) for f in shell.display_formatter.formatters.values() ]
  234. mplbackend = matplotlib.get_backend().lower()
  235. if mplbackend in ("nbagg", "ipympl", "widget", "module://ipympl.backend_nbagg"):
  236. formatter = shell.display_formatter.ipython_display_formatter
  237. formatter.for_type(Figure, _reshow_nbagg_figure)
  238. supported = {'png', 'png2x', 'retina', 'jpg', 'jpeg', 'svg', 'pdf'}
  239. bad = formats.difference(supported)
  240. if bad:
  241. bs = "%s" % ','.join([repr(f) for f in bad])
  242. gs = "%s" % ','.join([repr(f) for f in supported])
  243. raise ValueError("supported formats are: %s not %s" % (gs, bs))
  244. if "png" in formats:
  245. png_formatter.for_type(
  246. Figure, partial(print_figure, fmt="png", base64=True, **kwargs)
  247. )
  248. if "retina" in formats or "png2x" in formats:
  249. png_formatter.for_type(Figure, partial(retina_figure, base64=True, **kwargs))
  250. if "jpg" in formats or "jpeg" in formats:
  251. jpg_formatter.for_type(
  252. Figure, partial(print_figure, fmt="jpg", base64=True, **kwargs)
  253. )
  254. if "svg" in formats:
  255. svg_formatter.for_type(Figure, partial(print_figure, fmt="svg", **kwargs))
  256. if "pdf" in formats:
  257. pdf_formatter.for_type(
  258. Figure, partial(print_figure, fmt="pdf", base64=True, **kwargs)
  259. )
  260. #-----------------------------------------------------------------------------
  261. # Code for initializing matplotlib and importing pylab
  262. #-----------------------------------------------------------------------------
  263. def find_gui_and_backend(gui=None, gui_select=None):
  264. """Given a gui string return the gui and mpl backend.
  265. Parameters
  266. ----------
  267. gui : str
  268. Can be one of ('tk','gtk','wx','qt','qt4','inline','agg').
  269. gui_select : str
  270. Can be one of ('tk','gtk','wx','qt','qt4','inline').
  271. This is any gui already selected by the shell.
  272. Returns
  273. -------
  274. A tuple of (gui, backend) where backend is one of ('TkAgg','GTKAgg',
  275. 'WXAgg','Qt4Agg','module://matplotlib_inline.backend_inline','agg').
  276. """
  277. import matplotlib
  278. if _matplotlib_manages_backends():
  279. backend_registry = matplotlib.backends.registry.backend_registry
  280. # gui argument may be a gui event loop or may be a backend name.
  281. if gui in ("auto", None):
  282. backend = matplotlib.rcParamsOrig["backend"]
  283. backend, gui = backend_registry.resolve_backend(backend)
  284. else:
  285. gui = _convert_gui_to_matplotlib(gui)
  286. backend, gui = backend_registry.resolve_gui_or_backend(gui)
  287. gui = _convert_gui_from_matplotlib(gui)
  288. return gui, backend
  289. # Fallback to previous behaviour (Matplotlib < 3.9)
  290. mpl_version_info = getattr(matplotlib, "__version_info__", (0, 0))
  291. has_unified_qt_backend = mpl_version_info >= (3, 5)
  292. from IPython.core.pylabtools import backends
  293. backends_ = dict(backends)
  294. if not has_unified_qt_backend:
  295. backends_["qt"] = "qt5agg"
  296. if gui and gui != 'auto':
  297. # select backend based on requested gui
  298. backend = backends_[gui]
  299. if gui == 'agg':
  300. gui = None
  301. else:
  302. # We need to read the backend from the original data structure, *not*
  303. # from mpl.rcParams, since a prior invocation of %matplotlib may have
  304. # overwritten that.
  305. # WARNING: this assumes matplotlib 1.1 or newer!!
  306. backend = matplotlib.rcParamsOrig['backend']
  307. # In this case, we need to find what the appropriate gui selection call
  308. # should be for IPython, so we can activate inputhook accordingly
  309. from IPython.core.pylabtools import backend2gui
  310. gui = backend2gui.get(backend, None)
  311. # If we have already had a gui active, we need it and inline are the
  312. # ones allowed.
  313. if gui_select and gui != gui_select:
  314. gui = gui_select
  315. backend = backends_[gui]
  316. # Matplotlib before _matplotlib_manages_backends() can return "inline" for
  317. # no gui event loop rather than the None that IPython >= 8.24.0 expects.
  318. if gui == "inline":
  319. gui = None
  320. return gui, backend
  321. def activate_matplotlib(backend):
  322. """Activate the given backend and set interactive to True."""
  323. import matplotlib
  324. matplotlib.interactive(True)
  325. # Matplotlib had a bug where even switch_backend could not force
  326. # the rcParam to update. This needs to be set *before* the module
  327. # magic of switch_backend().
  328. matplotlib.rcParams['backend'] = backend
  329. # Due to circular imports, pyplot may be only partially initialised
  330. # when this function runs.
  331. # So avoid needing matplotlib attribute-lookup to access pyplot.
  332. from matplotlib import pyplot as plt
  333. plt.switch_backend(backend)
  334. plt.show._needmain = False
  335. # We need to detect at runtime whether show() is called by the user.
  336. # For this, we wrap it into a decorator which adds a 'called' flag.
  337. plt.draw_if_interactive = flag_calls(plt.draw_if_interactive)
  338. def import_pylab(user_ns, import_all=True):
  339. """Populate the namespace with pylab-related values.
  340. Imports matplotlib, pylab, numpy, and everything from pylab and numpy.
  341. Also imports a few names from IPython (figsize, display, getfigs)
  342. """
  343. # Import numpy as np/pyplot as plt are conventions we're trying to
  344. # somewhat standardize on. Making them available to users by default
  345. # will greatly help this.
  346. s = ("import numpy\n"
  347. "import matplotlib\n"
  348. "from matplotlib import pylab, mlab, pyplot\n"
  349. "np = numpy\n"
  350. "plt = pyplot\n"
  351. )
  352. exec(s, user_ns)
  353. if import_all:
  354. s = ("from matplotlib.pylab import *\n"
  355. "from numpy import *\n")
  356. exec(s, user_ns)
  357. # IPython symbols to add
  358. user_ns['figsize'] = figsize
  359. from IPython.display import display
  360. # Add display and getfigs to the user's namespace
  361. user_ns['display'] = display
  362. user_ns['getfigs'] = getfigs
  363. def configure_inline_support(shell, backend):
  364. """
  365. .. deprecated:: 7.23
  366. use `matplotlib_inline.backend_inline.configure_inline_support()`
  367. Configure an IPython shell object for matplotlib use.
  368. Parameters
  369. ----------
  370. shell : InteractiveShell instance
  371. backend : matplotlib backend
  372. """
  373. warnings.warn(
  374. "`configure_inline_support` is deprecated since IPython 7.23, directly "
  375. "use `matplotlib_inline.backend_inline.configure_inline_support()`",
  376. DeprecationWarning,
  377. stacklevel=2,
  378. )
  379. from matplotlib_inline.backend_inline import (
  380. configure_inline_support as configure_inline_support_orig,
  381. )
  382. configure_inline_support_orig(shell, backend)
  383. # Determine if Matplotlib manages backends only if needed, and cache result.
  384. # Do not read this directly, instead use _matplotlib_manages_backends().
  385. _matplotlib_manages_backends_value: bool | None = None
  386. def _matplotlib_manages_backends() -> bool:
  387. """Return True if Matplotlib manages backends, False otherwise.
  388. If it returns True, the caller can be sure that
  389. matplotlib.backends.registry.backend_registry is available along with
  390. member functions resolve_gui_or_backend, resolve_backend, list_all, and
  391. list_gui_frameworks.
  392. This function can be removed as it will always return True when Python
  393. 3.12, the latest version supported by Matplotlib < 3.9, reaches
  394. end-of-life in late 2028.
  395. """
  396. global _matplotlib_manages_backends_value
  397. if _matplotlib_manages_backends_value is None:
  398. try:
  399. from matplotlib.backends.registry import backend_registry
  400. _matplotlib_manages_backends_value = hasattr(
  401. backend_registry, "resolve_gui_or_backend"
  402. )
  403. except ImportError:
  404. _matplotlib_manages_backends_value = False
  405. return _matplotlib_manages_backends_value
  406. def _list_matplotlib_backends_and_gui_loops() -> list[str]:
  407. """Return list of all Matplotlib backends and GUI event loops.
  408. This is the list returned by
  409. %matplotlib --list
  410. """
  411. if _matplotlib_manages_backends():
  412. from matplotlib.backends.registry import backend_registry
  413. ret = backend_registry.list_all() + [
  414. _convert_gui_from_matplotlib(gui)
  415. for gui in backend_registry.list_gui_frameworks()
  416. ]
  417. else:
  418. from IPython.core import pylabtools
  419. ret = list(pylabtools.backends.keys())
  420. return sorted(["auto"] + ret)
  421. # Matplotlib and IPython do not always use the same gui framework name.
  422. # Always use the appropriate one of these conversion functions when passing a
  423. # gui framework name to/from Matplotlib.
  424. def _convert_gui_to_matplotlib(gui: str | None) -> str | None:
  425. if gui and gui.lower() == "osx":
  426. return "macosx"
  427. return gui
  428. def _convert_gui_from_matplotlib(gui: str | None) -> str | None:
  429. if gui and gui.lower() == "macosx":
  430. return "osx"
  431. return gui