Skip to content

Commit

Permalink
Merge 02dc7a4 into fc565b7
Browse files Browse the repository at this point in the history
  • Loading branch information
philippjfr committed Mar 16, 2018
2 parents fc565b7 + 02dc7a4 commit 7d3a576
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 49 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Expand Up @@ -29,7 +29,7 @@ install:
- conda info -a
- conda create -q -n test-environment python=$TRAVIS_PYTHON_VERSION flake8 scipy=1.0.0 numpy freetype nose pandas=0.22.0 jupyter ipython=4.2.0 param matplotlib=2.1.2 xarray
- source activate test-environment
- conda install -c conda-forge iris plotly flexx --quiet
- conda install -c conda-forge iris plotly flexx ffmpeg --quiet
- conda install -c bokeh datashader dask bokeh=0.12.14 selenium
- if [[ "$TRAVIS_PYTHON_VERSION" == "3.4" ]]; then
conda install python=3.4.3;
Expand Down
44 changes: 38 additions & 6 deletions examples/user_guide/Plotting_with_Matplotlib.ipynb
Expand Up @@ -133,16 +133,29 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"## Animated GIF support"
"## Animation support"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The ``'matplotlib'`` backend supports animated GIF output. This is useful for output to web pages that users can view without needing to interact with. It can also be useful for creating descriptive pages for HoloViews constructs that require a live Python/Jupyter server rather than just a web page - see for example [DynamicMap](../reference/containers/matplotlib/DynamicMap.ipynb).\n",
"The ``'matplotlib'`` backend supports animated outputs either as video (using mp4 or webm formats) or as animated GIFS. This is useful for output to web pages that users can view without needing to interact with. It can also be useful for creating descriptive pages for HoloViews constructs that require a live Python/Jupyter server rather than just a web page - see for example [DynamicMap](../reference/containers/matplotlib/DynamicMap.ipynb).\n",
"\n",
"Note that GIF support requires [ImageMagick](http://www.imagemagick.org) which is installed by default on many Linux installations and may be installed on OSX using [brew](http://brew.sh) . For more information on how to install ImageMagick (including Windows instructions) see the [installation page](http://www.imagemagick.org/script/binary-releases.php)."
"### GIF\n",
"\n",
"Note that GIF support requires [ImageMagick](http://www.imagemagick.org) which is installed by default on many Linux installations and may be installed on OSX using [brew](http://brew.sh) . For more information on how to install ImageMagick (including Windows instructions) see the [installation page](http://www.imagemagick.org/script/binary-releases.php).\n",
"\n",
"In recent versions of matplotlib (>=2.2.0) GIF output can also be generated using [pillow](https://pillow.readthedocs.io/en/latest/), which can be installed using ``conda install pillow`` or ``pip install pillow``.\n",
"\n",
"We can switch to 'gif' output in one of two ways, either by using the ``%%output`` magic setting the ``holomap='gif'`` option or by directly modifying the renderer with:\n",
"\n",
"```python\n",
"renderer = hv.renderer('matplotlib')\n",
"renderer.holomap = 'gif'\n",
"```\n",
"\n",
"Here we will plot an animation of the function we specified above, and also specify the ``fps`` (frames per second) to control the speed of the animation:"
]
},
{
Expand All @@ -152,16 +165,35 @@
"outputs": [],
"source": [
"%%opts Image [xaxis='bare', yaxis='bare' show_title=False, colorbar=True] (cmap='fire')\n",
"%%output holomap='gif'\n",
"%%output holomap='gif' fps=5\n",
"hv.HoloMap([(t, hv.Image(g(X,Y, 4 * np.sin(np.pi*t)))) for t in np.linspace(0,1,21)]) "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Videos\n",
"\n",
"Video output in matplotlib depends on ffmpeg, which may be [compiled from source](https://trac.ffmpeg.org/wiki/CompilationGuide), installed from conda using ``conda install ffmpeg``, or installed on OSX using brew using ``brew install ffmpeg``.\n",
"\n",
"To demonstrate we will plot the same animation as above but this time as a video:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"%%opts Image [xaxis='bare', yaxis='bare' show_title=False, colorbar=True] (cmap='fire')\n",
"%%output holomap='mp4'\n",
"hv.HoloMap([(t, hv.Image(g(X,Y, 4 * np.sin(np.pi*t)))) for t in np.linspace(0,1,21)]) "
]
}
],
"metadata": {
"language_info": {
"name": "python",
"pygments_lexer": "ipython3"
}
},
"nbformat": 4,
Expand Down
89 changes: 48 additions & 41 deletions holoviews/plotting/mpl/renderer.py
Expand Up @@ -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
Expand All @@ -28,6 +29,20 @@ class OutputWarning(param.Parameterized):pass
target.append(img)
"""

# <format name> : (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
Expand Down Expand Up @@ -64,15 +79,6 @@ class MPLRenderer(Renderer):

mode = param.ObjectSelector(default='default', objects=['default'])

# <format name> : (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',
Expand All @@ -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("<b>Python 3 matplotlib animation support broken &lt;= 1.3</b>")
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,
Expand Down Expand Up @@ -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("<b>Python 3 matplotlib animation support broken &lt;= 1.3</b>")
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]
Expand All @@ -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)

Expand Down
9 changes: 8 additions & 1 deletion tests/plotting/matplotlib/testrenderer.py
Expand Up @@ -66,4 +66,11 @@ 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("<img src='data:image/gif", data['text/html'])

@attr(optional=1) # Requires ffmpeg
def test_render_mp4(self):
data, metadata = self.renderer.components(self.map1, 'mp4')
self.assertIn("<source src='data:video/mp4", data['text/html'])

0 comments on commit 7d3a576

Please sign in to comment.