Thanks to visit codestin.com
Credit goes to github.com

Skip to content

ENH: Add support for FuncAnimation #687

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ environment:
PIP_DEPENDENCIES: "ipython"

matrix:
- PYTHON_VERSION: "3.5"
- PYTHON_VERSION: "3.6"
PYTHON_ARCH: "64"
- PYTHON_VERSION: "3.7"
PYTHON_ARCH: "64"
Expand Down
4 changes: 4 additions & 0 deletions doc/_static/theme_override.css
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,7 @@ a.sphx-glr-backref-module-sphinx_gallery {
text-decoration: underline;
background-color: #E6E6E6;
}

.anim-state label {
display: inline-block;
}
1 change: 1 addition & 0 deletions doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,7 @@ def setup(app):
# capture raw HTML or, if not present, __repr__ of last expression in
# each code block
'capture_repr': ('_repr_html_', '__repr__'),
'matplotlib_animations': True,
}

# Remove matplotlib agg warnings from generated doc when using plt.show
Expand Down
19 changes: 18 additions & 1 deletion doc/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -869,7 +869,24 @@ to scrape both matplotlib and Mayavi images you can do::

The default value is ``'image_scrapers': ('matplotlib',)`` which only scrapes
Matplotlib images. Note that this includes any images produced by packages that
are based on Matplotlib, for example Seaborn or Yellowbrick.
are based on Matplotlib, for example Seaborn or Yellowbrick. If you want
to embed :class:`matplotlib.animation.FuncAnimation`\s as animations rather
than a single static image of the animation figure, you should add::

sphinx_gallery_conf = {
...
'matplotlib_animations': True,
}

HTML embedding options can be changed by setting ``rcParams['animation.html']``
and related options in your
:ref:`matplotlib rcParams <matplotlib:matplotlib-rcparams>`.
It's also recommended to ensure that "imagemagick" is available as a
``writer``, which you can check with
:class:`matplotlib.animation.ImageMagickWriter.isAvailable()
<matplotlib.animation.ImageMagickWriter>`.
The FFmpeg writer in some light testing did not work as well for
creating GIF thumbnails for the gallery pages.

The following scrapers are supported:

Expand Down
25 changes: 25 additions & 0 deletions examples/plot_8_animations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""
Animation support
=================

Show an animation, which should end up nicely embedded below.
"""

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation

# Adapted from
# https://matplotlib.org/gallery/animation/basic_example.html


def _update_line(num):
line.set_data(data[..., :num])
return line,


fig, ax = plt.subplots()
data = np.random.RandomState(0).rand(2, 25)
line, = ax.plot([], [], 'r-')
ax.set(xlim=(0, 1), ylim=(0, 1))
ani = animation.FuncAnimation(fig, _update_line, 25, interval=100, blit=True)
9 changes: 9 additions & 0 deletions sphinx_gallery/_static/gallery.css
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,15 @@ ul.sphx-glr-horizontal img {
height: auto;
}

div.sphx-glr-animation {
margin: auto;
display: block;
max-width: 100%;
}
div.sphx-glr-animation .animation{
display: block;
}

p.sphx-glr-signature a.reference.external {
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
Expand Down
1 change: 1 addition & 0 deletions sphinx_gallery/gen_gallery.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
'inspect_global_variables': True,
'ignore_repr_types': r'',
'css': _KNOWN_CSS,
'matplotlib_animations': False,
}

logger = sphinx_compatibility.getLogger('sphinx-gallery')
Expand Down
2 changes: 1 addition & 1 deletion sphinx_gallery/gen_rst.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ def save_thumbnail(image_path_template, src_file, file_conf, gallery_conf):
img = gallery_conf.get("default_thumb_file", img)
else:
return
if ext == 'svg':
if ext in ('svg', 'gif'):
copyfile(img, thumb_file)
else:
scale_image(img, thumb_file, *gallery_conf["thumbnail_size"])
Expand Down
53 changes: 52 additions & 1 deletion sphinx_gallery/scrapers.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,17 @@ def _matplotlib_fig_titles(fig):
return fig_titles


_ANIMATION_RST = '''
.. only:: builder_html

.. container:: sphx-glr-animation

.. raw:: html

{0}
'''


def matplotlib_scraper(block, block_vars, gallery_conf, **kwargs):
"""Scrape Matplotlib images.

Expand All @@ -91,15 +102,32 @@ def matplotlib_scraper(block, block_vars, gallery_conf, **kwargs):
the images. This is often produced by :func:`figure_rst`.
"""
matplotlib, plt = _import_matplotlib()
from matplotlib.animation import FuncAnimation
image_path_iterator = block_vars['image_path_iterator']
image_rsts = []
# Check for animations
anims = list()
if gallery_conf.get('matplotlib_animations', False):
for ani in block_vars['example_globals'].values():
if isinstance(ani, FuncAnimation):
anims.append(ani)
# Then standard images
for fig_num, image_path in zip(plt.get_fignums(), image_path_iterator):
if 'format' in kwargs:
image_path = '%s.%s' % (os.path.splitext(image_path)[0],
kwargs['format'])
# Set the fig_num figure as the current figure as we can't
# save a figure that's not the current figure.
fig = plt.figure(fig_num)
# Deal with animations
cont = False
for anim in anims:
if anim._fig is fig:
image_rsts.append(_anim_rst(anim, image_path, gallery_conf))
cont = True
break
if cont:
continue
# get fig titles
fig_titles = _matplotlib_fig_titles(fig)
to_rgba = matplotlib.colors.colorConverter.to_rgba
Expand Down Expand Up @@ -129,6 +157,29 @@ def matplotlib_scraper(block, block_vars, gallery_conf, **kwargs):
return rst


def _anim_rst(anim, image_path, gallery_conf):
from matplotlib.animation import ImageMagickWriter
# output the thumbnail as the image, as it will just be copied
# if it's the file thumbnail
fig = anim._fig
image_path = image_path.replace('.png', '.gif')
fig_size = fig.get_size_inches()
thumb_size = gallery_conf['thumbnail_size']
use_dpi = round(
min(t_s / f_s for t_s, f_s in zip(thumb_size, fig_size)))
# FFmpeg is buggy for GIFs
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might want to see matplotlib/matplotlib#18093

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dopplershift would you be up for making a quick PR to triage based on the LooseVersion of matplotlib which backend to use?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. #733

if ImageMagickWriter.isAvailable():
writer = 'imagemagick'
else:
writer = None
anim.save(image_path, writer=writer, dpi=use_dpi)
html = anim._repr_html_()
if html is None: # plt.rcParams['animation.html'] == 'none'
html = anim.to_jshtml()
html = indent(html, ' ')
return _ANIMATION_RST.format(html)


def mayavi_scraper(block, block_vars, gallery_conf):
"""Scrape Mayavi images.

Expand Down Expand Up @@ -222,7 +273,7 @@ def __next__(self):


# For now, these are what we support
_KNOWN_IMG_EXTS = ('png', 'svg', 'jpg') # XXX add gif next
_KNOWN_IMG_EXTS = ('png', 'svg', 'jpg', 'gif')


def _find_image_ext(path):
Expand Down
30 changes: 19 additions & 11 deletions sphinx_gallery/tests/test_full.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

import pytest

N_TOT = 7
N_TOT = 8
N_FAILING = 1
N_GOOD = N_TOT - N_FAILING
N_RST = 14 + N_TOT
Expand Down Expand Up @@ -170,23 +170,31 @@ def test_image_formats(sphinx_app):
with codecs.open(generated_examples_index, 'r', 'utf-8') as fid:
html = fid.read()
thumb_fnames = ['../_images/sphx_glr_plot_svg_thumb.svg',
'../_images/sphx_glr_plot_numpy_matplotlib_thumb.png']
'../_images/sphx_glr_plot_numpy_matplotlib_thumb.png',
'../_images/sphx_glr_plot_animation_thumb.gif',
]
for thumb_fname in thumb_fnames:
file_fname = op.join(generated_examples_dir, thumb_fname)
assert op.isfile(file_fname)
assert op.isfile(file_fname), file_fname
want_html = 'src="%s"' % (thumb_fname,)
assert want_html in html
for ex, ext in (('plot_svg', 'svg'),
('plot_numpy_matplotlib', 'png'),
):
# the original GIF does not get copied because it's not used in the
# RST/HTML, so can't add it to this check
for ex, ext, nums, extra in (
('plot_svg', 'svg', [1], None),
('plot_numpy_matplotlib', 'png', [1], None),
('plot_animation', 'png', [1, 3], 'function Animation')):
html_fname = op.join(generated_examples_dir, '%s.html' % ex)
with codecs.open(html_fname, 'r', 'utf-8') as fid:
html = fid.read()
img_fname = '../_images/sphx_glr_%s_001.%s' % (ex, ext)
file_fname = op.join(generated_examples_dir, img_fname)
assert op.isfile(file_fname)
want_html = 'src="%s"' % (img_fname,)
assert want_html in html
for num in nums:
img_fname = '../_images/sphx_glr_%s_%03d.%s' % (ex, num, ext)
file_fname = op.join(generated_examples_dir, img_fname)
assert op.isfile(file_fname), file_fname
want_html = 'src="%s"' % (img_fname,)
assert want_html in html
if extra is not None:
assert extra in html


def test_embed_links_and_styles(sphinx_app):
Expand Down
1 change: 1 addition & 0 deletions sphinx_gallery/tests/tinybuild/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ def __call__(self, block, block_vars, gallery_conf):
'expected_failing_examples': ['examples/future/plot_future_imports_broken.py'], # noqa
'show_memory': True,
'junit': op.join('sphinx-gallery', 'junit-results.xml'),
'matplotlib_animations': True,
}
nitpicky = True
highlight_language = 'python3'
Expand Down
30 changes: 30 additions & 0 deletions sphinx_gallery/tests/tinybuild/examples/plot_animation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""
Animation support
=================

Show an animation, which should end up nicely embedded below.
"""

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation

# Adapted from
# https://matplotlib.org/gallery/animation/basic_example.html


def _update_line(num):
line.set_data(data[..., :num])
return line,


fig_0, ax_0 = plt.subplots(figsize=(5, 1))

# sphinx_gallery_thumbnail_number = 2
fig_1, ax_1 = plt.subplots(figsize=(5, 5))
data = np.random.RandomState(0).rand(2, 25)
line, = ax_1.plot([], [], 'r-')
ax_1.set(xlim=(0, 1), ylim=(0, 1))
ani = animation.FuncAnimation(fig_1, _update_line, 25, interval=100, blit=True)

fig_2, ax_2 = plt.subplots(figsize=(5, 5))
1 change: 1 addition & 0 deletions sphinx_gallery/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def scale_image(in_fname, out_fname, max_width, max_height):
# local import to avoid testing dependency on PIL:
Image = _get_image()
img = Image.open(in_fname)
# XXX someday we should just try img.thumbnail((max_width, max_height)) ...
width_in, height_in = img.size
scale_w = max_width / float(width_in)
scale_h = max_height / float(height_in)
Expand Down