From 34b6ea32e842a14896fcab610b1021f041208532 Mon Sep 17 00:00:00 2001
From: Ryan May <rmay31@gmail.com>
Date: Mon, 27 Jul 2015 23:20:37 -0500
Subject: [PATCH 1/5] Import JSAnimation into the animation module.

This pulls http://github.com/jakevdp/JSAnimation into the code. Most of
this is in the HTMLWriter class. This also adds the `jshtml` option for
the animation.html setting.
---
 lib/matplotlib/_animation_data.py | 210 ++++++++++++++++++++++++++++++
 lib/matplotlib/animation.py       | 146 +++++++++++++++++++++
 lib/matplotlib/rcsetup.py         |   7 +-
 matplotlibrc.template             |   1 +
 4 files changed, 362 insertions(+), 2 deletions(-)
 create mode 100644 lib/matplotlib/_animation_data.py

diff --git a/lib/matplotlib/_animation_data.py b/lib/matplotlib/_animation_data.py
new file mode 100644
index 000000000000..4c3f2c75b65e
--- /dev/null
+++ b/lib/matplotlib/_animation_data.py
@@ -0,0 +1,210 @@
+# Javascript template for HTMLWriter
+JS_INCLUDE = """
+<link rel="stylesheet"
+href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/
+css/font-awesome.min.css">
+<script language="javascript">
+  /* Define the Animation class */
+  function Animation(frames, img_id, slider_id, interval, loop_select_id){
+    this.img_id = img_id;
+    this.slider_id = slider_id;
+    this.loop_select_id = loop_select_id;
+    this.interval = interval;
+    this.current_frame = 0;
+    this.direction = 0;
+    this.timer = null;
+    this.frames = new Array(frames.length);
+
+    for (var i=0; i<frames.length; i++)
+    {
+     this.frames[i] = new Image();
+     this.frames[i].src = frames[i];
+    }
+    document.getElementById(this.slider_id).max = this.frames.length - 1;
+    this.set_frame(this.current_frame);
+  }
+
+  Animation.prototype.get_loop_state = function(){
+    var button_group = document[this.loop_select_id].state;
+    for (var i = 0; i < button_group.length; i++) {
+        var button = button_group[i];
+        if (button.checked) {
+            return button.value;
+        }
+    }
+    return undefined;
+  }
+
+  Animation.prototype.set_frame = function(frame){
+    this.current_frame = frame;
+    document.getElementById(this.img_id).src =
+            this.frames[this.current_frame].src;
+    document.getElementById(this.slider_id).value = this.current_frame;
+  }
+
+  Animation.prototype.next_frame = function()
+  {
+    this.set_frame(Math.min(this.frames.length - 1, this.current_frame + 1));
+  }
+
+  Animation.prototype.previous_frame = function()
+  {
+    this.set_frame(Math.max(0, this.current_frame - 1));
+  }
+
+  Animation.prototype.first_frame = function()
+  {
+    this.set_frame(0);
+  }
+
+  Animation.prototype.last_frame = function()
+  {
+    this.set_frame(this.frames.length - 1);
+  }
+
+  Animation.prototype.slower = function()
+  {
+    this.interval /= 0.7;
+    if(this.direction > 0){this.play_animation();}
+    else if(this.direction < 0){this.reverse_animation();}
+  }
+
+  Animation.prototype.faster = function()
+  {
+    this.interval *= 0.7;
+    if(this.direction > 0){this.play_animation();}
+    else if(this.direction < 0){this.reverse_animation();}
+  }
+
+  Animation.prototype.anim_step_forward = function()
+  {
+    this.current_frame += 1;
+    if(this.current_frame < this.frames.length){
+      this.set_frame(this.current_frame);
+    }else{
+      var loop_state = this.get_loop_state();
+      if(loop_state == "loop"){
+        this.first_frame();
+      }else if(loop_state == "reflect"){
+        this.last_frame();
+        this.reverse_animation();
+      }else{
+        this.pause_animation();
+        this.last_frame();
+      }
+    }
+  }
+
+  Animation.prototype.anim_step_reverse = function()
+  {
+    this.current_frame -= 1;
+    if(this.current_frame >= 0){
+      this.set_frame(this.current_frame);
+    }else{
+      var loop_state = this.get_loop_state();
+      if(loop_state == "loop"){
+        this.last_frame();
+      }else if(loop_state == "reflect"){
+        this.first_frame();
+        this.play_animation();
+      }else{
+        this.pause_animation();
+        this.first_frame();
+      }
+    }
+  }
+
+  Animation.prototype.pause_animation = function()
+  {
+    this.direction = 0;
+    if (this.timer){
+      clearInterval(this.timer);
+      this.timer = null;
+    }
+  }
+
+  Animation.prototype.play_animation = function()
+  {
+    this.pause_animation();
+    this.direction = 1;
+    var t = this;
+    if (!this.timer) this.timer = setInterval(function() {
+        t.anim_step_forward();
+    }, this.interval);
+  }
+
+  Animation.prototype.reverse_animation = function()
+  {
+    this.pause_animation();
+    this.direction = -1;
+    var t = this;
+    if (!this.timer) this.timer = setInterval(function() {
+        t.anim_step_reverse();
+    }, this.interval);
+  }
+</script>
+"""
+
+
+# HTML template for HTMLWriter
+DISPLAY_TEMPLATE = """
+<div class="animation" align="center">
+    <img id="_anim_img{id}">
+    <br>
+    <input id="_anim_slider{id}" type="range" style="width:350px"
+           name="points" min="0" max="1" step="1" value="0"
+           onchange="anim{id}.set_frame(parseInt(this.value));"></input>
+    <br>
+    <button onclick="anim{id}.slower()"><i class="fa fa-minus"></i></button>
+    <button onclick="anim{id}.first_frame()"><i class="fa fa-fast-backward">
+        </i></button>
+    <button onclick="anim{id}.previous_frame()">
+        <i class="fa fa-step-backward"></i></button>
+    <button onclick="anim{id}.reverse_animation()">
+        <i class="fa fa-play fa-flip-horizontal"></i></button>
+    <button onclick="anim{id}.pause_animation()"><i class="fa fa-pause">
+        </i></button>
+    <button onclick="anim{id}.play_animation()"><i class="fa fa-play"></i>
+        </button>
+    <button onclick="anim{id}.next_frame()"><i class="fa fa-step-forward">
+        </i></button>
+    <button onclick="anim{id}.last_frame()"><i class="fa fa-fast-forward">
+        </i></button>
+    <button onclick="anim{id}.faster()"><i class="fa fa-plus"></i></button>
+  <form action="#n" name="_anim_loop_select{id}" class="anim_control">
+    <input type="radio" name="state"
+           value="once" {once_checked}> Once </input>
+    <input type="radio" name="state"
+           value="loop" {loop_checked}> Loop </input>
+    <input type="radio" name="state"
+           value="reflect" {reflect_checked}> Reflect </input>
+  </form>
+</div>
+
+
+<script language="javascript">
+  /* Instantiate the Animation class. */
+  /* The IDs given should match those used in the template above. */
+  (function() {{
+    var img_id = "_anim_img{id}";
+    var slider_id = "_anim_slider{id}";
+    var loop_select_id = "_anim_loop_select{id}";
+    var frames = new Array({Nframes});
+    {fill_frames}
+
+    /* set a timeout to make sure all the above elements are created before
+       the object is initialized. */
+    setTimeout(function() {{
+        anim{id} = new Animation(frames, img_id, slider_id, {interval},
+                                 loop_select_id);
+    }}, 0);
+  }})()
+</script>
+"""
+
+INCLUDED_FRAMES = """
+  for (var i=0; i<{Nframes}; i++){{
+    frames[i] = "{frame_dir}/frame" + ("0000000" + i).slice(-7) +
+                ".{frame_format}";
+  }}
+"""
diff --git a/lib/matplotlib/animation.py b/lib/matplotlib/animation.py
index 6c80408a7219..6cf753f62b43 100644
--- a/lib/matplotlib/animation.py
+++ b/lib/matplotlib/animation.py
@@ -37,11 +37,18 @@
 import abc
 import contextlib
 import tempfile
+import uuid
 import warnings
+from matplotlib._animation_data import (DISPLAY_TEMPLATE, INCLUDED_FRAMES,
+                                        JS_INCLUDE)
 from matplotlib.cbook import iterable, deprecated
 from matplotlib.compat import subprocess
 from matplotlib import verbose
 from matplotlib import rcParams, rcParamsDefault, rc_context
+if sys.version_info < (3, 0):
+    from cStringIO import StringIO as InMemory
+else:
+    from io import BytesIO as InMemory
 
 # Process creation flag for subprocess to prevent it raising a terminal
 # window. See for example:
@@ -876,6 +883,112 @@ def _args(self):
                 + self.output_args)
 
 
+# Taken directly from jakevdp's JSAnimation package at
+# http://github.com/jakevdp/JSAnimation
+def _included_frames(frame_list, frame_format):
+    """frame_list should be a list of filenames"""
+    return INCLUDED_FRAMES.format(Nframes=len(frame_list),
+                                  frame_dir=os.path.dirname(frame_list[0]),
+                                  frame_format=frame_format)
+
+
+def _embedded_frames(frame_list, frame_format):
+    """frame_list should be a list of base64-encoded png files"""
+    template = '  frames[{0}] = "data:image/{1};base64,{2}"\n'
+    embedded = "\n"
+    for i, frame_data in enumerate(frame_list):
+        embedded += template.format(i, frame_format,
+                                    frame_data.replace('\n', '\\\n'))
+    return embedded
+
+
+@writers.register('html')
+class HTMLWriter(FileMovieWriter):
+    supported_formats = ['png', 'jpeg', 'tiff', 'svg']
+    args_key = 'animation.html_args'
+
+    @classmethod
+    def isAvailable(cls):
+        return True
+
+    def __init__(self, fps=30, codec=None, bitrate=None, extra_args=None,
+                 metadata=None, embed_frames=False, default_mode='loop'):
+        self.embed_frames = embed_frames
+        self.default_mode = default_mode.lower()
+
+        if self.default_mode not in ['loop', 'once', 'reflect']:
+            self.default_mode = 'loop'
+            import warnings
+            warnings.warn("unrecognized default_mode: using 'loop'")
+
+        self._saved_frames = list()
+        super(HTMLWriter, self).__init__(fps, codec, bitrate,
+                                         extra_args, metadata)
+
+    def setup(self, fig, outfile, dpi, frame_dir=None):
+        if os.path.splitext(outfile)[-1] not in ['.html', '.htm']:
+            raise ValueError("outfile must be *.htm or *.html")
+
+        if not self.embed_frames:
+            if frame_dir is None:
+                frame_dir = outfile.rstrip('.html') + '_frames'
+            if not os.path.exists(frame_dir):
+                os.makedirs(frame_dir)
+            frame_prefix = os.path.join(frame_dir, 'frame')
+        else:
+            frame_prefix = None
+
+        super(HTMLWriter, self).setup(fig, outfile, dpi,
+                                      frame_prefix, clear_temp=False)
+
+    def grab_frame(self, **savefig_kwargs):
+        if self.embed_frames:
+            suffix = '.' + self.frame_format
+            f = InMemory()
+            self.fig.savefig(f, format=self.frame_format,
+                             dpi=self.dpi, **savefig_kwargs)
+            f.seek(0)
+            imgdata64 = encodebytes(f.read()).decode('ascii')
+            self._saved_frames.append(imgdata64)
+        else:
+            return super(HTMLWriter, self).grab_frame(**savefig_kwargs)
+
+    def _run(self):
+        # make a duck-typed subprocess stand in
+        # this is called by the MovieWriter base class, but not used here.
+        class ProcessStandin(object):
+            returncode = 0
+
+            def communicate(self):
+                return '', ''
+
+        self._proc = ProcessStandin()
+
+        # save the frames to an html file
+        if self.embed_frames:
+            fill_frames = _embedded_frames(self._saved_frames,
+                                           self.frame_format)
+        else:
+            # temp names is filled by FileMovieWriter
+            fill_frames = _included_frames(self._temp_names,
+                                           self.frame_format)
+
+        mode_dict = dict(once_checked='',
+                         loop_checked='',
+                         reflect_checked='')
+        mode_dict[self.default_mode + '_checked'] = 'checked'
+
+        interval = int(1000. / self.fps)
+
+        with open(self.outfile, 'w') as of:
+            of.write(JS_INCLUDE)
+            of.write(DISPLAY_TEMPLATE.format(id=uuid.uuid4().hex,
+                                             Nframes=len(self._temp_names),
+                                             fill_frames=fill_frames,
+                                             interval=interval,
+                                             **mode_dict))
+
+
 class Animation(object):
     '''This class wraps the creation of an animation using matplotlib.
 
@@ -1288,11 +1401,44 @@ def to_html5_video(self):
                                 size=self._video_size,
                                 options=' '.join(options))
 
+    def to_jshtml(self, fps=None, embed_frames=True, default_mode=None):
+        """Generate HTML representation of the animation"""
+        if fps is None and hasattr(self, '_interval'):
+            # Convert interval in ms to frames per second
+            fps = 1000. / self._interval
+
+        # If we're not given a default mode, choose one base on the value of
+        # the repeat attribute
+        if default_mode is None:
+            default_mode = 'loop' if self.repeat else 'once'
+
+        if hasattr(self, "_html_representation"):
+            return self._html_representation
+        else:
+            # Can't open a second time while opened on windows. So we avoid
+            # deleting when closed, and delete manually later.
+            with tempfile.NamedTemporaryFile(suffix='.html',
+                                             delete=False) as f:
+                self.save(f.name, writer=HTMLWriter(fps=fps,
+                                                    embed_frames=embed_frames,
+                                                    default_mode=default_mode))
+            # Re-open and get content
+            with open(f.name) as fobj:
+                html = fobj.read()
+
+            # Now we can delete
+            os.remove(f.name)
+
+            self._html_representation = html
+            return html
+
     def _repr_html_(self):
         '''IPython display hook for rendering.'''
         fmt = rcParams['animation.html']
         if fmt == 'html5':
             return self.to_html5_video()
+        elif fmt == 'jshtml':
+            return self.to_jshtml()
 
 
 class TimedAnimation(Animation):
diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py
index 5bf8c5cee59e..b24557334b0b 100644
--- a/lib/matplotlib/rcsetup.py
+++ b/lib/matplotlib/rcsetup.py
@@ -602,7 +602,8 @@ def validate_hinting(s):
     ['ffmpeg', 'ffmpeg_file',
      'avconv', 'avconv_file',
      'mencoder', 'mencoder_file',
-     'imagemagick', 'imagemagick_file'])
+     'imagemagick', 'imagemagick_file',
+     'html'])
 
 validate_movie_frame_fmt = ValidateInStrings('animation.frame_format',
     ['png', 'jpeg', 'tiff', 'raw', 'rgba'])
@@ -610,7 +611,7 @@ def validate_hinting(s):
 validate_axis_locator = ValidateInStrings('major', ['minor', 'both', 'major'])
 
 validate_movie_html_fmt = ValidateInStrings('animation.html',
-    ['html5', 'none'])
+    ['html5', 'jshtml', 'none'])
 
 def validate_bbox(s):
     if isinstance(s, six.string_types):
@@ -1384,6 +1385,8 @@ def _validate_linestyle(ls):
     'animation.bitrate':      [-1, validate_int],
     # Controls image format when frames are written to disk
     'animation.frame_format': ['png', validate_movie_frame_fmt],
+    # Additional arguments for HTML writer
+    'animation.html_args':    [[], validate_stringlist],
     # Path to FFMPEG binary. If just binary name, subprocess uses $PATH.
     'animation.ffmpeg_path':  ['ffmpeg', validate_animation_writer_path],
 
diff --git a/matplotlibrc.template b/matplotlibrc.template
index 2a6e8b273fb5..991d200860e5 100644
--- a/matplotlibrc.template
+++ b/matplotlibrc.template
@@ -609,6 +609,7 @@ backend      : $TEMPLATE_BACKEND
 #animation.bitrate: -1             # Controls size/quality tradeoff for movie.
                                    # -1 implies let utility auto-determine
 #animation.frame_format: 'png'     # Controls frame format used by temp files
+#animation.html_args: ''           # Additional arguments to pass to html writer
 #animation.ffmpeg_path: 'ffmpeg'   # Path to ffmpeg binary. Without full path
                                    # $PATH is searched
 #animation.ffmpeg_args: ''         # Additional arguments to pass to ffmpeg

From 11f04648aa431ba01c71d1a7b8e435aaf5341fca Mon Sep 17 00:00:00 2001
From: Ryan May <rmay31@gmail.com>
Date: Tue, 4 Aug 2015 10:19:23 -0600
Subject: [PATCH 2/5] Limit size of HTML embedded animations.

This makes both the HTMLWriter and the HTML5 video support check the
amount of data being saved into HTML against the rc parameter
"animation.embed_limit".
---
 lib/matplotlib/animation.py | 75 ++++++++++++++++++++++++++++++-------
 lib/matplotlib/rcsetup.py   |  3 ++
 2 files changed, 64 insertions(+), 14 deletions(-)

diff --git a/lib/matplotlib/animation.py b/lib/matplotlib/animation.py
index 6cf753f62b43..e304eaed1c10 100644
--- a/lib/matplotlib/animation.py
+++ b/lib/matplotlib/animation.py
@@ -912,16 +912,28 @@ def isAvailable(cls):
         return True
 
     def __init__(self, fps=30, codec=None, bitrate=None, extra_args=None,
-                 metadata=None, embed_frames=False, default_mode='loop'):
+                 metadata=None, embed_frames=False, default_mode='loop',
+                 embed_limit=None):
         self.embed_frames = embed_frames
         self.default_mode = default_mode.lower()
 
+        # Save embed limit, which is given in MB
+        if embed_limit is None:
+            self._bytes_limit = rcParams['animation.embed_limit']
+        else:
+            self._bytes_limit = embed_limit
+
+        # Convert from MB to bytes
+        self._bytes_limit *= 1024 * 1024
+
         if self.default_mode not in ['loop', 'once', 'reflect']:
             self.default_mode = 'loop'
             import warnings
             warnings.warn("unrecognized default_mode: using 'loop'")
 
         self._saved_frames = list()
+        self._total_bytes = 0
+        self._hit_limit = False
         super(HTMLWriter, self).__init__(fps, codec, bitrate,
                                          extra_args, metadata)
 
@@ -943,13 +955,27 @@ def setup(self, fig, outfile, dpi, frame_dir=None):
 
     def grab_frame(self, **savefig_kwargs):
         if self.embed_frames:
+            # Just stop processing if we hit the limit
+            if self._hit_limit:
+                return
             suffix = '.' + self.frame_format
             f = InMemory()
             self.fig.savefig(f, format=self.frame_format,
                              dpi=self.dpi, **savefig_kwargs)
             f.seek(0)
             imgdata64 = encodebytes(f.read()).decode('ascii')
-            self._saved_frames.append(imgdata64)
+            self._total_bytes += len(imgdata64)
+            if self._total_bytes >= self._bytes_limit:
+                warnings.warn("Animation size has reached {0._total_bytes} "
+                              "bytes, exceeding the limit of "
+                              "{0._bytes_limit}. If you're sure you want "
+                              "a larger animation embedded, set the "
+                              "animation.embed_limit rc parameter to a "
+                              "larger value (in MB). This and further frames"
+                              " will be dropped.".format(self))
+                self._hit_limit = True
+            else:
+                self._saved_frames.append(imgdata64)
         else:
             return super(HTMLWriter, self).grab_frame(**savefig_kwargs)
 
@@ -1354,7 +1380,7 @@ def _end_redraw(self, evt):
         self._resize_id = self._fig.canvas.mpl_connect('resize_event',
                                                        self._handle_resize)
 
-    def to_html5_video(self):
+    def to_html5_video(self, embed_limit=None):
         '''Returns animation as an HTML5 video tag.
 
         This saves the animation as an h264 video, encoded in base64
@@ -1369,6 +1395,13 @@ def to_html5_video(self):
 </video>'''
         # Cache the rendering of the video as HTML
         if not hasattr(self, '_base64_video'):
+            # Save embed limit, which is given in MB
+            if embed_limit is None:
+                embed_limit = rcParams['animation.embed_limit']
+
+            # Convert from MB to bytes
+            embed_limit *= 1024 * 1024
+
             # First write the video to a tempfile. Set delete to False
             # so we can re-open to read binary data.
             with tempfile.NamedTemporaryFile(suffix='.m4v',
@@ -1384,22 +1417,36 @@ def to_html5_video(self):
             # Now open and base64 encode
             with open(f.name, 'rb') as video:
                 vid64 = encodebytes(video.read())
-                self._base64_video = vid64.decode('ascii')
-                self._video_size = 'width="{0}" height="{1}"'.format(
-                        *writer.frame_size)
+                vid_len = len(vid64)
+                if vid_len >= embed_limit:
+                    warnings.warn("Animation movie is {} bytes, exceeding "
+                                  "the limit of {}. If you're sure you want a "
+                                  "large animation embedded, set the "
+                                  "animation.embed_limit rc parameter to a "
+                                  "larger value (in MB).".format(vid_len,
+                                                                 embed_limit))
+                else:
+                    self._base64_video = vid64.decode('ascii')
+                    self._video_size = 'width="{}" height="{}"'.format(
+                            *writer.frame_size)
 
             # Now we can remove
             os.remove(f.name)
 
-        # Default HTML5 options are to autoplay and to display video controls
-        options = ['controls', 'autoplay']
+        # If we exceeded the size, this attribute won't exist
+        if hasattr(self, '_base64_video'):
+            # Default HTML5 options are to autoplay and display video controls
+            options = ['controls', 'autoplay']
 
-        # If we're set to repeat, make it loop
-        if self.repeat:
-            options.append('loop')
-        return VIDEO_TAG.format(video=self._base64_video,
-                                size=self._video_size,
-                                options=' '.join(options))
+            # If we're set to repeat, make it loop
+            if hasattr(self, 'repeat') and self.repeat:
+                options.append('loop')
+
+            return VIDEO_TAG.format(video=self._base64_video,
+                                    size=self._video_size,
+                                    options=' '.join(options))
+        else:
+            return 'Video too large to embed.'
 
     def to_jshtml(self, fps=None, embed_frames=True, default_mode=None):
         """Generate HTML representation of the animation"""
diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py
index b24557334b0b..e17f93874704 100644
--- a/lib/matplotlib/rcsetup.py
+++ b/lib/matplotlib/rcsetup.py
@@ -1380,6 +1380,9 @@ def _validate_linestyle(ls):
 
     # Animation settings
     'animation.html':         ['none', validate_movie_html_fmt],
+    # Limit, in MB, of size of base64 encoded animation in HTML
+    # (i.e. IPython notebook)
+    'animation.embed_limit':  [20, validate_float],
     'animation.writer':       ['ffmpeg', validate_movie_writer],
     'animation.codec':        ['h264', six.text_type],
     'animation.bitrate':      [-1, validate_int],

From c0c8fe3c008a37100f74dd06959da6f8842d7bb8 Mon Sep 17 00:00:00 2001
From: Ryan May <rmay31@gmail.com>
Date: Thu, 3 Dec 2015 09:33:14 -0700
Subject: [PATCH 3/5] Add What's New file for JSAnimation.

---
 doc/users/next_whats_new/js-animation.rst | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)
 create mode 100644 doc/users/next_whats_new/js-animation.rst

diff --git a/doc/users/next_whats_new/js-animation.rst b/doc/users/next_whats_new/js-animation.rst
new file mode 100644
index 000000000000..948c06ffceb6
--- /dev/null
+++ b/doc/users/next_whats_new/js-animation.rst
@@ -0,0 +1,16 @@
+Merge JSAnimation
+-----------------
+
+Jake Vanderplas' JSAnimation package has been merged into matplotlib. This
+adds to matplotlib the `~matplotlib.animation.HTMLWriter` class for
+generating a javascript HTML animation, suitable for the IPython notebook.
+This can be activated by default by setting the ``animation.html`` rc
+parameter to ``jshtml``. One can also call the ``anim_to_jshtml`` function
+to manually convert an animation. This can be displayed using IPython's
+``HTML`` display class::
+
+    from IPython.display import HTML
+    HTML(animation.anim_to_jshtml(anim))
+
+The `~matplotlib.animation.HTMLWriter` class can also be used to generate
+an HTML file by asking for the ``html`` writer.

From ace67007ae0e8698cbeb0e637b2342e1ccfa1177 Mon Sep 17 00:00:00 2001
From: Ryan May <rmay@ucar.edu>
Date: Mon, 8 May 2017 16:04:23 -0600
Subject: [PATCH 4/5] ENH: Add HTMLWriter to animation smoke tests

---
 lib/matplotlib/tests/test_animation.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/lib/matplotlib/tests/test_animation.py b/lib/matplotlib/tests/test_animation.py
index 017727016fe0..602b57384bb1 100644
--- a/lib/matplotlib/tests/test_animation.py
+++ b/lib/matplotlib/tests/test_animation.py
@@ -121,6 +121,7 @@ def isAvailable(self):
     ('avconv_file', 'mp4'),
     ('imagemagick', 'gif'),
     ('imagemagick_file', 'gif'),
+    ('html', 'html'),
     ('null', 'null')
 ]
 

From ffb2ccd5d69e1f68bdc383c42a05ee164b6f57c7 Mon Sep 17 00:00:00 2001
From: Ryan May <rmay31@gmail.com>
Date: Thu, 31 Aug 2017 18:01:54 -0600
Subject: [PATCH 5/5] Some cleanups to HTMLWriter.

---
 lib/matplotlib/animation.py | 10 ++++------
 1 file changed, 4 insertions(+), 6 deletions(-)

diff --git a/lib/matplotlib/animation.py b/lib/matplotlib/animation.py
index e304eaed1c10..0009706a8d05 100644
--- a/lib/matplotlib/animation.py
+++ b/lib/matplotlib/animation.py
@@ -928,10 +928,9 @@ def __init__(self, fps=30, codec=None, bitrate=None, extra_args=None,
 
         if self.default_mode not in ['loop', 'once', 'reflect']:
             self.default_mode = 'loop'
-            import warnings
             warnings.warn("unrecognized default_mode: using 'loop'")
 
-        self._saved_frames = list()
+        self._saved_frames = []
         self._total_bytes = 0
         self._hit_limit = False
         super(HTMLWriter, self).__init__(fps, codec, bitrate,
@@ -962,8 +961,7 @@ def grab_frame(self, **savefig_kwargs):
             f = InMemory()
             self.fig.savefig(f, format=self.frame_format,
                              dpi=self.dpi, **savefig_kwargs)
-            f.seek(0)
-            imgdata64 = encodebytes(f.read()).decode('ascii')
+            imgdata64 = encodebytes(f.getvalue()).decode('ascii')
             self._total_bytes += len(imgdata64)
             if self._total_bytes >= self._bytes_limit:
                 warnings.warn("Animation size has reached {0._total_bytes} "
@@ -1004,7 +1002,7 @@ def communicate(self):
                          reflect_checked='')
         mode_dict[self.default_mode + '_checked'] = 'checked'
 
-        interval = int(1000. / self.fps)
+        interval = 1000 // self.fps
 
         with open(self.outfile, 'w') as of:
             of.write(JS_INCLUDE)
@@ -1452,7 +1450,7 @@ def to_jshtml(self, fps=None, embed_frames=True, default_mode=None):
         """Generate HTML representation of the animation"""
         if fps is None and hasattr(self, '_interval'):
             # Convert interval in ms to frames per second
-            fps = 1000. / self._interval
+            fps = 1000 / self._interval
 
         # If we're not given a default mode, choose one base on the value of
         # the repeat attribute