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("