From ccd8495eab58f10d98302bbdf189be4dee6f7a9e Mon Sep 17 00:00:00 2001 From: tdpetrou Date: Mon, 16 Mar 2020 11:49:30 -0400 Subject: [PATCH 01/15] added png return for _repr_html_ for inline figures --- lib/matplotlib/figure.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 7c740ff39abf..97037e75e45b 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -376,6 +376,15 @@ def _repr_html_(self): from matplotlib.backends import backend_webagg return backend_webagg.ipython_inline_display(self) + if mpl.rcParams['backend'] == 'module://ipykernel.pylab.backend_inline': + from io import BytesIO + from base64 import b64encode + png_bytes = BytesIO() + self.canvas.print_figure(png_bytes, format='png') + s = png_bytes.getvalue() + s1 = b64encode(s).decode() + return f'' + def show(self, warn=True): """ If using a GUI backend with pyplot, display the figure window. From 0aff994309ac2a8f3db426e86a2dd961f4383797 Mon Sep 17 00:00:00 2001 From: tdpetrou Date: Mon, 16 Mar 2020 19:21:12 -0400 Subject: [PATCH 02/15] created _repr_html_ for FigureCanvasBase --- lib/matplotlib/backend_bases.py | 37 +++++++++++++++++++++++ lib/matplotlib/backends/backend_webagg.py | 3 ++ lib/matplotlib/figure.py | 15 +-------- 3 files changed, 41 insertions(+), 14 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index a4ce63879cbf..49a099d3c4b5 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -1701,6 +1701,43 @@ def __init__(self, figure): self.toolbar = None # NavigationToolbar2 will set me self._is_idle_drawing = False + def _repr_html_(self): + from base64 import b64encode + + fmt = self.get_default_filetype() + dpi = self.figure.dpi + if fmt == 'retina': + dpi = dpi * 2 + fmt = 'png' + + kw = { + "format":fmt, + "facecolor":self.figure.get_facecolor(), + "edgecolor":self.figure.get_edgecolor(), + "dpi":self.figure.dpi, + "bbox_inches":self.figure.bbox_inches, + } + + bytes_io = io.BytesIO() + self.print_figure(bytes_io, **kw) + raw_bytes = bytes_io.getvalue() + + mimetype_dict = {'png': 'image/png', 'svg': 'image/svg+xml', + 'jpg': 'image/jpeg', 'pdf': 'application/pdf'} + if fmt not in mimetype_dict: + fmt = 'png' + mimetype = mimetype_dict[fmt] + + if fmt == 'svg': + return raw_bytes.decode() + elif fmt == 'pdf': + data = b64encode(raw_bytes).decode() + w, h = self.figure.get_size_inches() + return f'' + else: + data = b64encode(raw_bytes).decode() + return f'' + @classmethod @functools.lru_cache() def _fix_ipython_backend2gui(cls): diff --git a/lib/matplotlib/backends/backend_webagg.py b/lib/matplotlib/backends/backend_webagg.py index 555563b1f34f..9572ec9bf26f 100644 --- a/lib/matplotlib/backends/backend_webagg.py +++ b/lib/matplotlib/backends/backend_webagg.py @@ -50,6 +50,9 @@ def run(self): class FigureCanvasWebAgg(core.FigureCanvasWebAggCore): _timer_cls = TimerTornado + def _repr_html_(self): + return ipython_inline_display(self.figure) + def show(self): # show the figure window global show # placates pyflakes: created by @_Backend.export below diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 97037e75e45b..42e24d2ffa54 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -370,20 +370,7 @@ def __init__(self, # use it, for some reason. def _repr_html_(self): - # We can't use "isinstance" here, because then we'd end up importing - # webagg unconditionally. - if 'WebAgg' in type(self.canvas).__name__: - from matplotlib.backends import backend_webagg - return backend_webagg.ipython_inline_display(self) - - if mpl.rcParams['backend'] == 'module://ipykernel.pylab.backend_inline': - from io import BytesIO - from base64 import b64encode - png_bytes = BytesIO() - self.canvas.print_figure(png_bytes, format='png') - s = png_bytes.getvalue() - s1 = b64encode(s).decode() - return f'' + return self.canvas._repr_html_() def show(self, warn=True): """ From 7cf97241b3b09bfe133fe1498e2a3c0a23db472b Mon Sep 17 00:00:00 2001 From: tdpetrou Date: Mon, 16 Mar 2020 19:31:30 -0400 Subject: [PATCH 03/15] using calculated dpi --- lib/matplotlib/backend_bases.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 49a099d3c4b5..53883080df85 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -1714,7 +1714,7 @@ def _repr_html_(self): "format":fmt, "facecolor":self.figure.get_facecolor(), "edgecolor":self.figure.get_edgecolor(), - "dpi":self.figure.dpi, + "dpi":dpi, "bbox_inches":self.figure.bbox_inches, } From 934db096c61ed7bac00739604d8cab942168a453 Mon Sep 17 00:00:00 2001 From: tdpetrou Date: Tue, 17 Mar 2020 15:58:14 -0400 Subject: [PATCH 04/15] corrected for retina --- lib/matplotlib/backend_bases.py | 62 ++++++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 16 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 53883080df85..ae47b3074bce 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -1702,41 +1702,71 @@ def __init__(self, figure): self._is_idle_drawing = False def _repr_html_(self): - from base64 import b64encode + if not self.figure.axes and not self.figure.lines: + return - fmt = self.get_default_filetype() dpi = self.figure.dpi - if fmt == 'retina': - dpi = dpi * 2 - fmt = 'png' + is_retina = False + + import IPython + ip = IPython.get_ipython() + ib_list = [c for c in ip.configurables + if 'InlineBackend' in type(c).__name__] + + # having an 'inline' backend doesn't mean '%matplotlib inline' has been run + # Running %matplotlib inline runs pylabtools.configure_inline_support + # which appends the InlineBackend to the list of configurables + if get_backend() == 'module://ipykernel.pylab.backend_inline' and ib_list: + ib = ib_list[0] + bbox_inches = ib.print_figure_kwargs['bbox_inches'] + fmt = next(iter(ib.figure_formats)) + if fmt == 'retina': + is_retina = True + dpi = dpi * 2 + fmt = self.get_default_filetype() + else: + bbox_inches = 'tight' # how to let user choose self.figure.bbox_inches? + fmt = self.get_default_filetype() + + if fmt not in {'png', 'svg', 'jpg', 'pdf'}: + fmt = 'png' kw = { "format":fmt, "facecolor":self.figure.get_facecolor(), "edgecolor":self.figure.get_edgecolor(), "dpi":dpi, - "bbox_inches":self.figure.bbox_inches, + "bbox_inches":bbox_inches, } bytes_io = io.BytesIO() self.print_figure(bytes_io, **kw) raw_bytes = bytes_io.getvalue() - mimetype_dict = {'png': 'image/png', 'svg': 'image/svg+xml', - 'jpg': 'image/jpeg', 'pdf': 'application/pdf'} - if fmt not in mimetype_dict: - fmt = 'png' - mimetype = mimetype_dict[fmt] + from IPython.core.display import _pngxy, _jpegxy if fmt == 'svg': return raw_bytes.decode() + elif fmt == 'png': + w, h = _pngxy(raw_bytes) + elif fmt == 'jpg': + w, h = _jpegxy(raw_bytes) elif fmt == 'pdf': - data = b64encode(raw_bytes).decode() w, h = self.figure.get_size_inches() - return f'' - else: - data = b64encode(raw_bytes).decode() - return f'' + w, h = w * dpi, h * dpi + + if is_retina: + w, h = w // 2, h // 2 + + from base64 import b64encode + data = b64encode(raw_bytes).decode() + + if fmt == 'png': + return f'' + elif fmt == 'pdf': + return f'' + elif fmt == 'jpg': + return f'' @classmethod @functools.lru_cache() From ff1384c35d3809c7d452118192dbee8faf9f65c7 Mon Sep 17 00:00:00 2001 From: tdpetrou Date: Wed, 18 Mar 2020 10:09:17 -0400 Subject: [PATCH 05/15] defer to ipython for html whenever possible --- lib/matplotlib/backend_bases.py | 66 ++++++++++----------------------- 1 file changed, 19 insertions(+), 47 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index ae47b3074bce..8168c899e1e9 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -1702,71 +1702,43 @@ def __init__(self, figure): self._is_idle_drawing = False def _repr_html_(self): - if not self.figure.axes and not self.figure.lines: - return - - dpi = self.figure.dpi - is_retina = False - - import IPython - ip = IPython.get_ipython() - ib_list = [c for c in ip.configurables - if 'InlineBackend' in type(c).__name__] - - # having an 'inline' backend doesn't mean '%matplotlib inline' has been run - # Running %matplotlib inline runs pylabtools.configure_inline_support - # which appends the InlineBackend to the list of configurables - if get_backend() == 'module://ipykernel.pylab.backend_inline' and ib_list: - ib = ib_list[0] - bbox_inches = ib.print_figure_kwargs['bbox_inches'] - fmt = next(iter(ib.figure_formats)) - if fmt == 'retina': - is_retina = True - dpi = dpi * 2 - fmt = self.get_default_filetype() - else: - bbox_inches = 'tight' # how to let user choose self.figure.bbox_inches? - fmt = self.get_default_filetype() - - if fmt not in {'png', 'svg', 'jpg', 'pdf'}: - fmt = 'png' + # Defer to IPython to handle html output if possible + if 'IPython' in sys.modules: + import IPython + ip = IPython.get_ipython() + # Check whether %matplotlib was run. Is there a better way? + ib_list = [c for c in ip.configurables + if 'InlineBackend' in type(c).__name__] + if ib_list: + return + + fmt = self.get_default_filetype() kw = { "format":fmt, "facecolor":self.figure.get_facecolor(), "edgecolor":self.figure.get_edgecolor(), - "dpi":dpi, - "bbox_inches":bbox_inches, + "dpi":self.figure.dpi, + "bbox_inches":self.figure.bbox_inches } bytes_io = io.BytesIO() self.print_figure(bytes_io, **kw) raw_bytes = bytes_io.getvalue() - from IPython.core.display import _pngxy, _jpegxy - + from base64 import b64encode + data = b64encode(raw_bytes).decode() + if fmt == 'svg': return raw_bytes.decode() elif fmt == 'png': - w, h = _pngxy(raw_bytes) - elif fmt == 'jpg': - w, h = _jpegxy(raw_bytes) + return f'' elif fmt == 'pdf': w, h = self.figure.get_size_inches() - w, h = w * dpi, h * dpi - - if is_retina: - w, h = w // 2, h // 2 - - from base64 import b64encode - data = b64encode(raw_bytes).decode() - - if fmt == 'png': - return f'' - elif fmt == 'pdf': + w, h = w * self.figure.dpi, h * self.figure.dpi return f'' elif fmt == 'jpg': - return f'' + return f'' @classmethod @functools.lru_cache() From 1f94c51ebe23bce7a6735332fe292f26e8984724 Mon Sep 17 00:00:00 2001 From: Bradley Dice Date: Sat, 11 Jul 2020 16:25:38 -0500 Subject: [PATCH 06/15] Use _repr_png_ with other suggested PR changes. --- lib/matplotlib/backend_bases.py | 50 ++++++++++----------------------- lib/matplotlib/figure.py | 8 ++---- 2 files changed, 17 insertions(+), 41 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 8168c899e1e9..b278af5fe5bd 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -1701,44 +1701,24 @@ def __init__(self, figure): self.toolbar = None # NavigationToolbar2 will set me self._is_idle_drawing = False - def _repr_html_(self): - # Defer to IPython to handle html output if possible + def _repr_png_(self): + # Defer to IPython to handle output if possible. if 'IPython' in sys.modules: - import IPython - ip = IPython.get_ipython() - # Check whether %matplotlib was run. Is there a better way? - ib_list = [c for c in ip.configurables - if 'InlineBackend' in type(c).__name__] - if ib_list: + from IPython.core.pylabtools import configure_inline_support + # Check whether %matplotlib was run. + if hasattr(configure_inline_support, 'current_backend'): return - - fmt = self.get_default_filetype() - - kw = { - "format":fmt, - "facecolor":self.figure.get_facecolor(), - "edgecolor":self.figure.get_edgecolor(), - "dpi":self.figure.dpi, - "bbox_inches":self.figure.bbox_inches - } - bytes_io = io.BytesIO() - self.print_figure(bytes_io, **kw) - raw_bytes = bytes_io.getvalue() - - from base64 import b64encode - data = b64encode(raw_bytes).decode() - - if fmt == 'svg': - return raw_bytes.decode() - elif fmt == 'png': - return f'' - elif fmt == 'pdf': - w, h = self.figure.get_size_inches() - w, h = w * self.figure.dpi, h * self.figure.dpi - return f'' - elif fmt == 'jpg': - return f'' + png_bytes = io.BytesIO() + self.print_figure( + png_bytes, + format='png', + facecolor=self.figure.get_facecolor(), + edgecolor=self.figure.get_edgecolor(), + dpi=self.figure.dpi, + bbox_inches=self.figure.bbox_inches + ) + return png_bytes.getvalue() @classmethod @functools.lru_cache() diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 42e24d2ffa54..1bf5f93626a3 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -365,12 +365,8 @@ def __init__(self, # list of child gridspecs for this figure self._gridspecs = [] - # TODO: I'd like to dynamically add the _repr_html_ method - # to the figure in the right context, but then IPython doesn't - # use it, for some reason. - - def _repr_html_(self): - return self.canvas._repr_html_() + def _repr_png_(self): + return self.canvas._repr_png_() def show(self, warn=True): """ From 919a32d07a25c39a6d4d90bb562f3f32c7bfe4d6 Mon Sep 17 00:00:00 2001 From: Bradley Dice Date: Sat, 11 Jul 2020 16:42:19 -0500 Subject: [PATCH 07/15] Add test. --- lib/matplotlib/tests/test_figure.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/matplotlib/tests/test_figure.py b/lib/matplotlib/tests/test_figure.py index dc04a869ce7b..5085d511dab4 100644 --- a/lib/matplotlib/tests/test_figure.py +++ b/lib/matplotlib/tests/test_figure.py @@ -781,3 +781,9 @@ def test_fail(self, x, match): def test_hashable_keys(self, fig_test, fig_ref): fig_test.subplot_mosaic([[object(), object()]]) fig_ref.subplot_mosaic([["A", "B"]]) + +def test_figure_repr_png(): + from matplotlib.figure import Figure + fig = Figure(figsize=(4, 2)) + ax = fig.add_subplot() + fig._repr_png_() From 963afe909bf8cc1487ee0012f38d477de6137932 Mon Sep 17 00:00:00 2001 From: Bradley Dice Date: Sat, 11 Jul 2020 16:43:45 -0500 Subject: [PATCH 08/15] Ensure png is non-empty. --- lib/matplotlib/tests/test_figure.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/tests/test_figure.py b/lib/matplotlib/tests/test_figure.py index 5085d511dab4..84b5791a68c7 100644 --- a/lib/matplotlib/tests/test_figure.py +++ b/lib/matplotlib/tests/test_figure.py @@ -786,4 +786,5 @@ def test_figure_repr_png(): from matplotlib.figure import Figure fig = Figure(figsize=(4, 2)) ax = fig.add_subplot() - fig._repr_png_() + png_bytes = fig._repr_png_() + assert len(png_bytes) > 0 From ec9b2dd3422d497b02f914ce18c6bcfaa81bcdf4 Mon Sep 17 00:00:00 2001 From: Bradley Dice Date: Sat, 11 Jul 2020 17:00:27 -0500 Subject: [PATCH 09/15] Add docstring to _repr_png_. --- lib/matplotlib/figure.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 1bf5f93626a3..95150bdc5b5b 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -366,6 +366,7 @@ def __init__(self, self._gridspecs = [] def _repr_png_(self): + """Generate a PNG representation of the Figure.""" return self.canvas._repr_png_() def show(self, warn=True): From dafe7e8700c1a7d0b891415ab17cf24cbc4c89f6 Mon Sep 17 00:00:00 2001 From: Bradley Dice Date: Sat, 11 Jul 2020 17:00:35 -0500 Subject: [PATCH 10/15] Add missing newline. --- lib/matplotlib/tests/test_figure.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/matplotlib/tests/test_figure.py b/lib/matplotlib/tests/test_figure.py index 84b5791a68c7..ad6e2a028636 100644 --- a/lib/matplotlib/tests/test_figure.py +++ b/lib/matplotlib/tests/test_figure.py @@ -782,6 +782,7 @@ def test_hashable_keys(self, fig_test, fig_ref): fig_test.subplot_mosaic([[object(), object()]]) fig_ref.subplot_mosaic([["A", "B"]]) + def test_figure_repr_png(): from matplotlib.figure import Figure fig = Figure(figsize=(4, 2)) From e44fe0f61b4a317f66b0019f3d1641f72fdf2483 Mon Sep 17 00:00:00 2001 From: Bradley Dice Date: Sat, 11 Jul 2020 17:14:20 -0500 Subject: [PATCH 11/15] Deprecate ipython_inline_display. --- lib/matplotlib/backends/backend_webagg.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/backends/backend_webagg.py b/lib/matplotlib/backends/backend_webagg.py index 9572ec9bf26f..0b2d26b9cbaf 100644 --- a/lib/matplotlib/backends/backend_webagg.py +++ b/lib/matplotlib/backends/backend_webagg.py @@ -33,6 +33,7 @@ import tornado.websocket import matplotlib as mpl +from matplotlib import cbook from matplotlib.backend_bases import _Backend from matplotlib._pylab_helpers import Gcf from . import backend_webagg_core as core @@ -50,9 +51,6 @@ def run(self): class FigureCanvasWebAgg(core.FigureCanvasWebAggCore): _timer_cls = TimerTornado - def _repr_html_(self): - return ipython_inline_display(self.figure) - def show(self): # show the figure window global show # placates pyflakes: created by @_Backend.export below @@ -285,6 +283,7 @@ def catch_sigint(): ioloop.start() +@cbook.deprecated("3.4") def ipython_inline_display(figure): import tornado.template From e10495f27bac1a2447c8749ba694a95f3287c1bb Mon Sep 17 00:00:00 2001 From: Bradley Dice Date: Sat, 11 Jul 2020 17:19:15 -0500 Subject: [PATCH 12/15] Add deprecation notice for ipython_inline_display. --- doc/api/next_api_changes/deprecations/17891-BDD.rst | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 doc/api/next_api_changes/deprecations/17891-BDD.rst diff --git a/doc/api/next_api_changes/deprecations/17891-BDD.rst b/doc/api/next_api_changes/deprecations/17891-BDD.rst new file mode 100644 index 000000000000..907daf7167f7 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/17891-BDD.rst @@ -0,0 +1,6 @@ +Removing ``ipython_inline_display`` from Webagg backend +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The function ``matplotlib.backends.backend_webagg.ipython_inline_display`` is +deprecated. It relies on outdated functionality of the tornado server and +asyncio event loop and appears to be broken. From 27dc45d3c3c2ebfb33cfbabb3ad4be33bc19cca4 Mon Sep 17 00:00:00 2001 From: Bradley Dice Date: Sat, 11 Jul 2020 17:26:20 -0500 Subject: [PATCH 13/15] Ensure the canvas has a _repr_png_ defined. --- lib/matplotlib/figure.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 95150bdc5b5b..6a546ceeaab4 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -367,7 +367,8 @@ def __init__(self, def _repr_png_(self): """Generate a PNG representation of the Figure.""" - return self.canvas._repr_png_() + if hasattr(self.canvas, '_repr_png_'): + return self.canvas._repr_png_() def show(self, warn=True): """ From cfbbded55c97270c50527e04e5550d06e7ed9076 Mon Sep 17 00:00:00 2001 From: Bradley Dice Date: Sat, 11 Jul 2020 17:42:20 -0500 Subject: [PATCH 14/15] Add docstring. --- lib/matplotlib/backend_bases.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index b278af5fe5bd..c909d5bf8bc1 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -1702,6 +1702,7 @@ def __init__(self, figure): self._is_idle_drawing = False def _repr_png_(self): + """Generate a PNG representation of the FigureCanvasBase.""" # Defer to IPython to handle output if possible. if 'IPython' in sys.modules: from IPython.core.pylabtools import configure_inline_support From c6e27e34329c1c5c6be2385a3ac8cd14417e8336 Mon Sep 17 00:00:00 2001 From: Bradley Dice Date: Sat, 11 Jul 2020 17:42:28 -0500 Subject: [PATCH 15/15] Add What's New. --- doc/users/next_whats_new/figure_repr_png.rst | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 doc/users/next_whats_new/figure_repr_png.rst diff --git a/doc/users/next_whats_new/figure_repr_png.rst b/doc/users/next_whats_new/figure_repr_png.rst new file mode 100644 index 000000000000..fb2656d119b4 --- /dev/null +++ b/doc/users/next_whats_new/figure_repr_png.rst @@ -0,0 +1,7 @@ +Add ``_repr_png_`` for figures with no configured IPython backend +----------------------------------------------------------------- + +Previously, IPython would not show figures as images unless using the +``matplotlib.pyplot`` interface or with an IPython magic statement like +``%matplotlib backend``. Now, no magic is required to view PNG figure +representations.