312 lines
11 KiB
Python
312 lines
11 KiB
Python
"""A matplotlib backend for publishing figures via display_data"""
|
|
|
|
# Copyright (c) IPython Development Team.
|
|
# Distributed under the terms of the BSD 3-Clause License.
|
|
|
|
import matplotlib
|
|
from matplotlib import colors
|
|
from matplotlib.backends import backend_agg
|
|
from matplotlib.backends.backend_agg import FigureCanvasAgg
|
|
from matplotlib._pylab_helpers import Gcf
|
|
from matplotlib.figure import Figure
|
|
|
|
from IPython.core.interactiveshell import InteractiveShell
|
|
from IPython.core.getipython import get_ipython
|
|
from IPython.core.pylabtools import select_figure_formats
|
|
from IPython.display import display
|
|
|
|
from .config import InlineBackend
|
|
|
|
|
|
def new_figure_manager(num, *args, FigureClass=Figure, **kwargs):
|
|
"""
|
|
Return a new figure manager for a new figure instance.
|
|
|
|
This function is part of the API expected by Matplotlib backends.
|
|
"""
|
|
return new_figure_manager_given_figure(num, FigureClass(*args, **kwargs))
|
|
|
|
|
|
def new_figure_manager_given_figure(num, figure):
|
|
"""
|
|
Return a new figure manager for a given figure instance.
|
|
|
|
This function is part of the API expected by Matplotlib backends.
|
|
"""
|
|
manager = backend_agg.new_figure_manager_given_figure(num, figure)
|
|
|
|
# Hack: matplotlib FigureManager objects in interacive backends (at least
|
|
# in some of them) monkeypatch the figure object and add a .show() method
|
|
# to it. This applies the same monkeypatch in order to support user code
|
|
# that might expect `.show()` to be part of the official API of figure
|
|
# objects. For further reference:
|
|
# https://github.com/ipython/ipython/issues/1612
|
|
# https://github.com/matplotlib/matplotlib/issues/835
|
|
|
|
if not hasattr(figure, 'show'):
|
|
# Queue up `figure` for display
|
|
figure.show = lambda *a: display(
|
|
figure, metadata=_fetch_figure_metadata(figure))
|
|
|
|
# If matplotlib was manually set to non-interactive mode, this function
|
|
# should be a no-op (otherwise we'll generate duplicate plots, since a user
|
|
# who set ioff() manually expects to make separate draw/show calls).
|
|
if not matplotlib.is_interactive():
|
|
return manager
|
|
|
|
# ensure current figure will be drawn, and each subsequent call
|
|
# of draw_if_interactive() moves the active figure to ensure it is
|
|
# drawn last
|
|
try:
|
|
show._to_draw.remove(figure)
|
|
except ValueError:
|
|
# ensure it only appears in the draw list once
|
|
pass
|
|
# Queue up the figure for drawing in next show() call
|
|
show._to_draw.append(figure)
|
|
show._draw_called = True
|
|
|
|
return manager
|
|
|
|
|
|
def show(close=None, block=None):
|
|
"""Show all figures as SVG/PNG payloads sent to the IPython clients.
|
|
|
|
Parameters
|
|
----------
|
|
close : bool, optional
|
|
If true, a ``plt.close('all')`` call is automatically issued after
|
|
sending all the figures. If this is set, the figures will entirely
|
|
removed from the internal list of figures.
|
|
block : Not used.
|
|
The `block` parameter is a Matplotlib experimental parameter.
|
|
We accept it in the function signature for compatibility with other
|
|
backends.
|
|
"""
|
|
if close is None:
|
|
close = InlineBackend.instance().close_figures
|
|
try:
|
|
for figure_manager in Gcf.get_all_fig_managers():
|
|
display(
|
|
figure_manager.canvas.figure,
|
|
metadata=_fetch_figure_metadata(figure_manager.canvas.figure)
|
|
)
|
|
finally:
|
|
show._to_draw = []
|
|
# only call close('all') if any to close
|
|
# close triggers gc.collect, which can be slow
|
|
if close and Gcf.get_all_fig_managers():
|
|
matplotlib.pyplot.close('all')
|
|
|
|
|
|
# This flag will be reset by draw_if_interactive when called
|
|
show._draw_called = False
|
|
# list of figures to draw when flush_figures is called
|
|
show._to_draw = []
|
|
|
|
|
|
def flush_figures():
|
|
"""Send all figures that changed
|
|
|
|
This is meant to be called automatically and will call show() if, during
|
|
prior code execution, there had been any calls to draw_if_interactive.
|
|
|
|
This function is meant to be used as a post_execute callback in IPython,
|
|
so user-caused errors are handled with showtraceback() instead of being
|
|
allowed to raise. If this function is not called from within IPython,
|
|
then these exceptions will raise.
|
|
"""
|
|
if not show._draw_called:
|
|
return
|
|
|
|
try:
|
|
if InlineBackend.instance().close_figures:
|
|
# ignore the tracking, just draw and close all figures
|
|
try:
|
|
return show(True)
|
|
except Exception as e:
|
|
# safely show traceback if in IPython, else raise
|
|
ip = get_ipython()
|
|
if ip is None:
|
|
raise e
|
|
else:
|
|
ip.showtraceback()
|
|
return
|
|
|
|
# exclude any figures that were closed:
|
|
active = set([fm.canvas.figure for fm in Gcf.get_all_fig_managers()])
|
|
for fig in [fig for fig in show._to_draw if fig in active]:
|
|
try:
|
|
display(fig, metadata=_fetch_figure_metadata(fig))
|
|
except Exception as e:
|
|
# safely show traceback if in IPython, else raise
|
|
ip = get_ipython()
|
|
if ip is None:
|
|
raise e
|
|
else:
|
|
ip.showtraceback()
|
|
return
|
|
finally:
|
|
# clear flags for next round
|
|
show._to_draw = []
|
|
show._draw_called = False
|
|
|
|
|
|
# Changes to matplotlib in version 1.2 requires a mpl backend to supply a default
|
|
# figurecanvas. This is set here to a Agg canvas
|
|
# See https://github.com/matplotlib/matplotlib/pull/1125
|
|
FigureCanvas = FigureCanvasAgg
|
|
|
|
|
|
def configure_inline_support(shell, backend):
|
|
"""Configure an IPython shell object for matplotlib use.
|
|
|
|
Parameters
|
|
----------
|
|
shell : InteractiveShell instance
|
|
|
|
backend : matplotlib backend
|
|
"""
|
|
# If using our svg payload backend, register the post-execution
|
|
# function that will pick up the results for display. This can only be
|
|
# done with access to the real shell object.
|
|
|
|
cfg = InlineBackend.instance(parent=shell)
|
|
cfg.shell = shell
|
|
if cfg not in shell.configurables:
|
|
shell.configurables.append(cfg)
|
|
|
|
if backend in ('inline', 'module://matplotlib_inline.backend_inline'):
|
|
shell.events.register('post_execute', flush_figures)
|
|
|
|
# Save rcParams that will be overwrittern
|
|
shell._saved_rcParams = {}
|
|
for k in cfg.rc:
|
|
shell._saved_rcParams[k] = matplotlib.rcParams[k]
|
|
# load inline_rc
|
|
matplotlib.rcParams.update(cfg.rc)
|
|
new_backend_name = "inline"
|
|
else:
|
|
try:
|
|
shell.events.unregister('post_execute', flush_figures)
|
|
except ValueError:
|
|
pass
|
|
if hasattr(shell, '_saved_rcParams'):
|
|
matplotlib.rcParams.update(shell._saved_rcParams)
|
|
del shell._saved_rcParams
|
|
new_backend_name = "other"
|
|
|
|
# only enable the formats once -> don't change the enabled formats (which the user may
|
|
# has changed) when getting another "%matplotlib inline" call.
|
|
# See https://github.com/ipython/ipykernel/issues/29
|
|
cur_backend = getattr(configure_inline_support, "current_backend", "unset")
|
|
if new_backend_name != cur_backend:
|
|
# Setup the default figure format
|
|
select_figure_formats(shell, cfg.figure_formats, **cfg.print_figure_kwargs)
|
|
configure_inline_support.current_backend = new_backend_name
|
|
|
|
|
|
def _enable_matplotlib_integration():
|
|
"""Enable extra IPython matplotlib integration when we are loaded as the matplotlib backend."""
|
|
from matplotlib import get_backend
|
|
ip = get_ipython()
|
|
backend = get_backend()
|
|
if ip and backend in ('inline', 'module://matplotlib_inline.backend_inline'):
|
|
from IPython.core.pylabtools import activate_matplotlib
|
|
try:
|
|
activate_matplotlib(backend)
|
|
configure_inline_support(ip, backend)
|
|
except (ImportError, AttributeError):
|
|
# bugs may cause a circular import on Python 2
|
|
def configure_once(*args):
|
|
activate_matplotlib(backend)
|
|
configure_inline_support(ip, backend)
|
|
ip.events.unregister('post_run_cell', configure_once)
|
|
ip.events.register('post_run_cell', configure_once)
|
|
|
|
|
|
_enable_matplotlib_integration()
|
|
|
|
|
|
def _fetch_figure_metadata(fig):
|
|
"""Get some metadata to help with displaying a figure."""
|
|
# determine if a background is needed for legibility
|
|
if _is_transparent(fig.get_facecolor()):
|
|
# the background is transparent
|
|
ticksLight = _is_light([label.get_color()
|
|
for axes in fig.axes
|
|
for axis in (axes.xaxis, axes.yaxis)
|
|
for label in axis.get_ticklabels()])
|
|
if ticksLight.size and (ticksLight == ticksLight[0]).all():
|
|
# there are one or more tick labels, all with the same lightness
|
|
return {'needs_background': 'dark' if ticksLight[0] else 'light'}
|
|
|
|
return None
|
|
|
|
|
|
def _is_light(color):
|
|
"""Determines if a color (or each of a sequence of colors) is light (as
|
|
opposed to dark). Based on ITU BT.601 luminance formula (see
|
|
https://stackoverflow.com/a/596241)."""
|
|
rgbaArr = colors.to_rgba_array(color)
|
|
return rgbaArr[:, :3].dot((.299, .587, .114)) > .5
|
|
|
|
|
|
def _is_transparent(color):
|
|
"""Determine transparency from alpha."""
|
|
rgba = colors.to_rgba(color)
|
|
return rgba[3] < .5
|
|
|
|
|
|
def set_matplotlib_formats(*formats, **kwargs):
|
|
"""Select figure formats for the inline backend. Optionally pass quality for JPEG.
|
|
|
|
For example, this enables PNG and JPEG output with a JPEG quality of 90%::
|
|
|
|
In [1]: set_matplotlib_formats('png', 'jpeg', quality=90)
|
|
|
|
To set this in your config files use the following::
|
|
|
|
c.InlineBackend.figure_formats = {'png', 'jpeg'}
|
|
c.InlineBackend.print_figure_kwargs.update({'quality' : 90})
|
|
|
|
Parameters
|
|
----------
|
|
*formats : strs
|
|
One or more figure formats to enable: 'png', 'retina', 'jpeg', 'svg', 'pdf'.
|
|
**kwargs
|
|
Keyword args will be relayed to ``figure.canvas.print_figure``.
|
|
"""
|
|
# build kwargs, starting with InlineBackend config
|
|
cfg = InlineBackend.instance()
|
|
kw = {}
|
|
kw.update(cfg.print_figure_kwargs)
|
|
kw.update(**kwargs)
|
|
shell = InteractiveShell.instance()
|
|
select_figure_formats(shell, formats, **kw)
|
|
|
|
|
|
def set_matplotlib_close(close=True):
|
|
"""Set whether the inline backend closes all figures automatically or not.
|
|
|
|
By default, the inline backend used in the IPython Notebook will close all
|
|
matplotlib figures automatically after each cell is run. This means that
|
|
plots in different cells won't interfere. Sometimes, you may want to make
|
|
a plot in one cell and then refine it in later cells. This can be accomplished
|
|
by::
|
|
|
|
In [1]: set_matplotlib_close(False)
|
|
|
|
To set this in your config files use the following::
|
|
|
|
c.InlineBackend.close_figures = False
|
|
|
|
Parameters
|
|
----------
|
|
close : bool
|
|
Should all matplotlib figures be automatically closed after each cell is
|
|
run?
|
|
"""
|
|
cfg = InlineBackend.instance()
|
|
cfg.close_figures = close
|