From 8b1f8b5d340fef16eeafb058c68ac8182c911575 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 16 Mar 2018 15:17:49 +0000 Subject: [PATCH] Fixed regression embedding matplotlib animation output --- holoviews/plotting/mpl/renderer.py | 89 ++++++++++++----------- tests/plotting/matplotlib/testrenderer.py | 13 +++- 2 files changed, 60 insertions(+), 42 deletions(-) diff --git a/holoviews/plotting/mpl/renderer.py b/holoviews/plotting/mpl/renderer.py index 47ba439d3c..a1747ed28c 100644 --- a/holoviews/plotting/mpl/renderer.py +++ b/holoviews/plotting/mpl/renderer.py @@ -4,6 +4,7 @@ from tempfile import NamedTemporaryFile from contextlib import contextmanager from itertools import chain +from distutils.version import LooseVersion import matplotlib as mpl from matplotlib import pyplot as plt @@ -28,6 +29,20 @@ class OutputWarning(param.Parameterized):pass target.append(img) """ +# : (animation writer, format, anim_kwargs, extra_args) +ANIMATION_OPTS = { + 'webm': ('ffmpeg', 'webm', {}, + ['-vcodec', 'libvpx-vp9', '-b', '1000k']), + 'mp4': ('ffmpeg', 'mp4', {'codec': 'libx264'}, + ['-pix_fmt', 'yuv420p']), + 'gif': ('imagemagick', 'gif', {'fps': 10}, []), + 'scrubber': ('html', None, {'fps': 5}, None) +} + +if LooseVersion(mpl.__version__) >= '2.2': + ANIMATION_OPTS['gif'] = ('pillow', 'gif', {'fps': 10}, []) + + class MPLRenderer(Renderer): """ Exporter used to render data from matplotlib, either to a stream @@ -64,15 +79,6 @@ class MPLRenderer(Renderer): mode = param.ObjectSelector(default='default', objects=['default']) - # : (animation writer, format, anim_kwargs, extra_args) - ANIMATION_OPTS = { - 'webm': ('ffmpeg', 'webm', {}, - ['-vcodec', 'libvpx', '-b', '1000k']), - 'mp4': ('ffmpeg', 'mp4', {'codec': 'libx264'}, - ['-pix_fmt', 'yuv420p']), - 'gif': ('imagemagick', 'gif', {'fps': 10}, []), - 'scrubber': ('html', None, {'fps': 5}, None) - } mode_formats = {'fig': {'default': ['png', 'svg', 'pdf', 'html', None, 'auto']}, 'holomap': {'default': ['widgets', 'scrubber', 'webm','mp4', 'gif', @@ -97,15 +103,9 @@ def __call__(self, obj, fmt='auto'): if isinstance(plot, tuple(self.widgets.values())): data = plot() - elif fmt in ['png', 'svg', 'pdf', 'html', 'json']: - with mpl.rc_context(rc=plot.fig_rcparams): - data = self._figure_data(plot, fmt, **({'dpi':self.dpi} if self.dpi else {})) else: - if sys.version_info[0] == 3 and mpl.__version__[:-2] in ['1.2', '1.3']: - raise Exception("Python 3 matplotlib animation support broken <= 1.3") with mpl.rc_context(rc=plot.fig_rcparams): - anim = plot.anim(fps=self.fps) - data = self._anim_data(anim, fmt) + data = self._figure_data(plot, fmt, **({'dpi':self.dpi} if self.dpi else {})) data = self._apply_post_render_hooks(data, obj, fmt) return data, {'file-ext':fmt, @@ -188,31 +188,38 @@ def _figure_data(self, plot, fmt='png', bbox_inches='tight', as_script=False, ** Similar to IPython.core.pylabtools.print_figure but without any IPython dependency. """ - fig = plot.state - - traverse_fn = lambda x: x.handles.get('bbox_extra_artists', None) - extra_artists = list(chain(*[artists for artists in plot.traverse(traverse_fn) - if artists is not None])) - - kw = dict( - format=fmt, - facecolor=fig.get_facecolor(), - edgecolor=fig.get_edgecolor(), - dpi=self.dpi, - bbox_inches=bbox_inches, - bbox_extra_artists=extra_artists - ) - kw.update(kwargs) - - # Attempts to precompute the tight bounding box - try: - kw = self._compute_bbox(fig, kw) - except: - pass + if fmt in ['gif', 'mp4', 'webm']: + if sys.version_info[0] == 3 and mpl.__version__[:-2] in ['1.2', '1.3']: + raise Exception("Python 3 matplotlib animation support broken <= 1.3") + with mpl.rc_context(rc=plot.fig_rcparams): + anim = plot.anim(fps=self.fps) + data = self._anim_data(anim, fmt) + else: + fig = plot.state + + traverse_fn = lambda x: x.handles.get('bbox_extra_artists', None) + extra_artists = list(chain(*[artists for artists in plot.traverse(traverse_fn) + if artists is not None])) + + kw = dict( + format=fmt, + facecolor=fig.get_facecolor(), + edgecolor=fig.get_edgecolor(), + dpi=self.dpi, + bbox_inches=bbox_inches, + bbox_extra_artists=extra_artists + ) + kw.update(kwargs) + + # Attempts to precompute the tight bounding box + try: + kw = self._compute_bbox(fig, kw) + except: + pass + bytes_io = BytesIO() + fig.canvas.print_figure(bytes_io, **kw) + data = bytes_io.getvalue() - bytes_io = BytesIO() - fig.canvas.print_figure(bytes_io, **kw) - data = bytes_io.getvalue() if as_script: b64 = base64.b64encode(data).decode("utf-8") (mime_type, tag) = MIME_TYPES[fmt], HTML_TAGS[fmt] @@ -228,7 +235,7 @@ def _anim_data(self, anim, fmt): """ Render a matplotlib animation object and return the corresponding data. """ - (writer, _, anim_kwargs, extra_args) = self.ANIMATION_OPTS[fmt] + (writer, _, anim_kwargs, extra_args) = ANIMATION_OPTS[fmt] if extra_args != []: anim_kwargs = dict(anim_kwargs, extra_args=extra_args) diff --git a/tests/plotting/matplotlib/testrenderer.py b/tests/plotting/matplotlib/testrenderer.py index d5ad3e0d77..f51f9ec4fa 100644 --- a/tests/plotting/matplotlib/testrenderer.py +++ b/tests/plotting/matplotlib/testrenderer.py @@ -66,4 +66,15 @@ def test_get_size_table(self): w, h = self.renderer.get_size(plot) self.assertEqual((w, h), (288, 288)) - + def test_render_gif(self): + data, metadata = self.renderer.components(self.map1, 'gif') + self.assertIn("