Skip to content

Commit 56fba88

Browse files
committed
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.
1 parent 7539b61 commit 56fba88

File tree

10 files changed

+369
-4
lines changed

10 files changed

+369
-4
lines changed

lib/matplotlib/animation.py

Lines changed: 362 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,13 @@
2929
import itertools
3030
import base64
3131
import contextlib
32+
import random
33+
import string
3234
import tempfile
35+
if sys.version_info < (3, 0):
36+
from cStringIO import StringIO as InMemory
37+
else:
38+
from io import BytesIO as InMemory
3339
from matplotlib.cbook import iterable, is_string_like
3440
from matplotlib.compat import subprocess
3541
from matplotlib import verbose
@@ -561,6 +567,360 @@ def _args(self):
561567
+ self.output_args)
562568

563569

570+
ICON_DIR = os.path.join(os.path.dirname(__file__), 'mpl-data', 'images')
571+
572+
573+
class _Icons(object):
574+
"""This class is a container for base64 representations of the icons"""
575+
icons = ['first', 'prev', 'reverse', 'pause', 'play', 'next', 'last']
576+
577+
def __init__(self, icon_dir=ICON_DIR, extension='png'):
578+
self.icon_dir = icon_dir
579+
self.extension = extension
580+
for icon in self.icons:
581+
setattr(self, icon,
582+
self._load_base64('{0}.{1}'.format(icon, extension)))
583+
584+
def _load_base64(self, filename):
585+
data = open(os.path.join(self.icon_dir, filename), 'rb').read()
586+
imgdata64 = base64.b64encode(data).decode('ascii')
587+
return 'data:image/{0};base64,{1}'.format(self.extension, imgdata64)
588+
589+
590+
JS_INCLUDE = """
591+
<script language="javascript">
592+
/* Define the Animation class */
593+
function Animation(frames, img_id, slider_id, interval, loop_select_id){
594+
this.img_id = img_id;
595+
this.slider_id = slider_id;
596+
this.loop_select_id = loop_select_id;
597+
this.interval = interval;
598+
this.current_frame = 0;
599+
this.direction = 0;
600+
this.timer = null;
601+
this.frames = new Array(frames.length);
602+
603+
for (var i=0; i<frames.length; i++)
604+
{
605+
this.frames[i] = new Image();
606+
this.frames[i].src = frames[i];
607+
}
608+
document.getElementById(this.slider_id).max = this.frames.length - 1;
609+
this.set_frame(this.current_frame);
610+
}
611+
612+
Animation.prototype.get_loop_state = function(){
613+
var button_group = document[this.loop_select_id].state;
614+
for (var i = 0; i < button_group.length; i++) {
615+
var button = button_group[i];
616+
if (button.checked) {
617+
return button.value;
618+
}
619+
}
620+
return undefined;
621+
}
622+
623+
Animation.prototype.set_frame = function(frame){
624+
this.current_frame = frame;
625+
document.getElementById(this.img_id).src = this.frames[this.current_frame].src;
626+
document.getElementById(this.slider_id).value = this.current_frame;
627+
}
628+
629+
Animation.prototype.next_frame = function()
630+
{
631+
this.set_frame(Math.min(this.frames.length - 1, this.current_frame + 1));
632+
}
633+
634+
Animation.prototype.previous_frame = function()
635+
{
636+
this.set_frame(Math.max(0, this.current_frame - 1));
637+
}
638+
639+
Animation.prototype.first_frame = function()
640+
{
641+
this.set_frame(0);
642+
}
643+
644+
Animation.prototype.last_frame = function()
645+
{
646+
this.set_frame(this.frames.length - 1);
647+
}
648+
649+
Animation.prototype.slower = function()
650+
{
651+
this.interval /= 0.7;
652+
if(this.direction > 0){this.play_animation();}
653+
else if(this.direction < 0){this.reverse_animation();}
654+
}
655+
656+
Animation.prototype.faster = function()
657+
{
658+
this.interval *= 0.7;
659+
if(this.direction > 0){this.play_animation();}
660+
else if(this.direction < 0){this.reverse_animation();}
661+
}
662+
663+
Animation.prototype.anim_step_forward = function()
664+
{
665+
this.current_frame += 1;
666+
if(this.current_frame < this.frames.length){
667+
this.set_frame(this.current_frame);
668+
}else{
669+
var loop_state = this.get_loop_state();
670+
if(loop_state == "loop"){
671+
this.first_frame();
672+
}else if(loop_state == "reflect"){
673+
this.last_frame();
674+
this.reverse_animation();
675+
}else{
676+
this.pause_animation();
677+
this.last_frame();
678+
}
679+
}
680+
}
681+
682+
Animation.prototype.anim_step_reverse = function()
683+
{
684+
this.current_frame -= 1;
685+
if(this.current_frame >= 0){
686+
this.set_frame(this.current_frame);
687+
}else{
688+
var loop_state = this.get_loop_state();
689+
if(loop_state == "loop"){
690+
this.last_frame();
691+
}else if(loop_state == "reflect"){
692+
this.first_frame();
693+
this.play_animation();
694+
}else{
695+
this.pause_animation();
696+
this.first_frame();
697+
}
698+
}
699+
}
700+
701+
Animation.prototype.pause_animation = function()
702+
{
703+
this.direction = 0;
704+
if (this.timer){
705+
clearInterval(this.timer);
706+
this.timer = null;
707+
}
708+
}
709+
710+
Animation.prototype.play_animation = function()
711+
{
712+
this.pause_animation();
713+
this.direction = 1;
714+
var t = this;
715+
if (!this.timer) this.timer = setInterval(function(){t.anim_step_forward();}, this.interval);
716+
}
717+
718+
Animation.prototype.reverse_animation = function()
719+
{
720+
this.pause_animation();
721+
this.direction = -1;
722+
var t = this;
723+
if (!this.timer) this.timer = setInterval(function(){t.anim_step_reverse();}, this.interval);
724+
}
725+
</script>
726+
"""
727+
728+
729+
DISPLAY_TEMPLATE = """
730+
<div class="animation" align="center">
731+
<img id="_anim_img{id}">
732+
<br>
733+
<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>
734+
<br>
735+
<button onclick="anim{id}.slower()">&#8211;</button>
736+
<button onclick="anim{id}.first_frame()"><img class="anim_icon" src="{icons.first}"></button>
737+
<button onclick="anim{id}.previous_frame()"><img class="anim_icon" src="{icons.prev}"></button>
738+
<button onclick="anim{id}.reverse_animation()"><img class="anim_icon" src="{icons.reverse}"></button>
739+
<button onclick="anim{id}.pause_animation()"><img class="anim_icon" src="{icons.pause}"></button>
740+
<button onclick="anim{id}.play_animation()"><img class="anim_icon" src="{icons.play}"></button>
741+
<button onclick="anim{id}.next_frame()"><img class="anim_icon" src="{icons.next}"></button>
742+
<button onclick="anim{id}.last_frame()"><img class="anim_icon" src="{icons.last}"></button>
743+
<button onclick="anim{id}.faster()">+</button>
744+
<form action="#n" name="_anim_loop_select{id}" class="anim_control">
745+
<input type="radio" name="state" value="once" {once_checked}> Once </input>
746+
<input type="radio" name="state" value="loop" {loop_checked}> Loop </input>
747+
<input type="radio" name="state" value="reflect" {reflect_checked}> Reflect </input>
748+
</form>
749+
</div>
750+
751+
752+
<script language="javascript">
753+
/* Instantiate the Animation class. */
754+
/* The IDs given should match those used in the template above. */
755+
(function() {{
756+
var img_id = "_anim_img{id}";
757+
var slider_id = "_anim_slider{id}";
758+
var loop_select_id = "_anim_loop_select{id}";
759+
var frames = new Array({Nframes});
760+
{fill_frames}
761+
762+
/* set a timeout to make sure all the above elements are created before
763+
the object is initialized. */
764+
setTimeout(function() {{
765+
anim{id} = new Animation(frames, img_id, slider_id, {interval}, loop_select_id);
766+
}}, 0);
767+
}})()
768+
</script>
769+
"""
770+
771+
INCLUDED_FRAMES = """
772+
for (var i=0; i<{Nframes}; i++){{
773+
frames[i] = "{frame_dir}/frame" + ("0000000" + i).slice(-7) + ".{frame_format}";
774+
}}
775+
"""
776+
777+
778+
def _included_frames(frame_list, frame_format):
779+
"""frame_list should be a list of filenames"""
780+
return INCLUDED_FRAMES.format(Nframes=len(frame_list),
781+
frame_dir=os.path.dirname(frame_list[0]),
782+
frame_format=frame_format)
783+
784+
785+
def _embedded_frames(frame_list, frame_format):
786+
"""frame_list should be a list of base64-encoded png files"""
787+
template = ' frames[{0}] = "data:image/{1};base64,{2}"\n'
788+
embedded = "\n"
789+
for i, frame_data in enumerate(frame_list):
790+
embedded += template.format(i, frame_format,
791+
frame_data.replace('\n', '\\\n'))
792+
return embedded
793+
794+
795+
# Taken directly from jakevdp's JSAnimation package at
796+
# http://github.com/jakevdp/JSAnimation
797+
@writers.register('html')
798+
class HTMLWriter(FileMovieWriter):
799+
# we start the animation id count at a random number: this way, if two
800+
# animations are meant to be included on one HTML page, there is a
801+
# very small chance of conflict.
802+
rng = random.Random()
803+
supported_formats = ['png', 'jpeg', 'tiff', 'svg']
804+
args_key = 'animation.html_args'
805+
806+
@classmethod
807+
def isAvailable(cls):
808+
return True
809+
810+
@classmethod
811+
def new_id(cls):
812+
#return '%16x' % cls.rng.getrandbits(64)
813+
return ''.join(cls.rng.choice(string.ascii_uppercase)
814+
for x in range(16))
815+
816+
def __init__(self, fps=30, codec=None, bitrate=None, extra_args=None,
817+
metadata=None, embed_frames=False, default_mode='loop'):
818+
self.embed_frames = embed_frames
819+
self.default_mode = default_mode.lower()
820+
821+
if self.default_mode not in ['loop', 'once', 'reflect']:
822+
self.default_mode = 'loop'
823+
import warnings
824+
warnings.warn("unrecognized default_mode: using 'loop'")
825+
826+
self._saved_frames = list()
827+
super(HTMLWriter, self).__init__(fps, codec, bitrate,
828+
extra_args, metadata)
829+
830+
def setup(self, fig, outfile, dpi, frame_dir=None):
831+
if os.path.splitext(outfile)[-1] not in ['.html', '.htm']:
832+
raise ValueError("outfile must be *.htm or *.html")
833+
834+
if not self.embed_frames:
835+
if frame_dir is None:
836+
frame_dir = outfile.rstrip('.html') + '_frames'
837+
if not os.path.exists(frame_dir):
838+
os.makedirs(frame_dir)
839+
frame_prefix = os.path.join(frame_dir, 'frame')
840+
else:
841+
frame_prefix = None
842+
843+
super(HTMLWriter, self).setup(fig, outfile, dpi,
844+
frame_prefix, clear_temp=False)
845+
846+
def grab_frame(self, **savefig_kwargs):
847+
if self.embed_frames:
848+
suffix = '.' + self.frame_format
849+
f = InMemory()
850+
self.fig.savefig(f, format=self.frame_format,
851+
dpi=self.dpi, **savefig_kwargs)
852+
f.seek(0)
853+
imgdata64 = base64.b64encode(f.read()).decode('ascii')
854+
self._saved_frames.append(imgdata64)
855+
else:
856+
return super(HTMLWriter, self).grab_frame(**savefig_kwargs)
857+
858+
def _run(self):
859+
# make a ducktyped subprocess standin
860+
# this is called by the MovieWriter base class, but not used here.
861+
class ProcessStandin(object):
862+
returncode = 0
863+
def communicate(self):
864+
return ('', '')
865+
self._proc = ProcessStandin()
866+
867+
# save the frames to an html file
868+
if self.embed_frames:
869+
fill_frames = _embedded_frames(self._saved_frames,
870+
self.frame_format)
871+
else:
872+
# temp names is filled by FileMovieWriter
873+
fill_frames = _included_frames(self._temp_names,
874+
self.frame_format)
875+
876+
mode_dict = dict(once_checked='',
877+
loop_checked='',
878+
reflect_checked='')
879+
mode_dict[self.default_mode + '_checked'] = 'checked'
880+
881+
interval = int(1000. / self.fps)
882+
883+
with open(self.outfile, 'w') as of:
884+
of.write(JS_INCLUDE)
885+
of.write(DISPLAY_TEMPLATE.format(id=self.new_id(),
886+
Nframes=len(self._temp_names),
887+
fill_frames=fill_frames,
888+
interval=interval,
889+
icons=_Icons(),
890+
**mode_dict))
891+
892+
893+
def anim_to_jshtml(anim, fps=None, embed_frames=True, default_mode=None):
894+
"""Generate HTML representation of the animation"""
895+
if fps is None and hasattr(anim, '_interval'):
896+
# Convert interval in ms to frames per second
897+
fps = 1000. / anim._interval
898+
899+
# If we're not given a default mode, choose one base on the value of
900+
# the repeat attribute
901+
if default_mode is None:
902+
default_mode = 'loop' if anim.repeat else 'once'
903+
904+
if hasattr(anim, "_html_representation"):
905+
return anim._html_representation
906+
else:
907+
# Can't open a second time while opened on windows. So we avoid
908+
# deleting when closed, and delete manually later.
909+
with tempfile.NamedTemporaryFile(suffix='.html', delete=False) as f:
910+
anim.save(f.name, writer=HTMLWriter(fps=fps,
911+
embed_frames=embed_frames,
912+
default_mode=default_mode))
913+
# Re-open and get content
914+
with open(f.name) as fobj:
915+
html = fobj.read()
916+
917+
# Now we can delete
918+
os.remove(f.name)
919+
920+
anim._html_representation = html
921+
return html
922+
923+
564924
class Animation(object):
565925
'''
566926
This class wraps the creation of an animation using matplotlib. It is
@@ -939,6 +1299,8 @@ def _repr_html_(self):
9391299
fmt = rcParams['animation.html']
9401300
if fmt == 'html5':
9411301
return self.to_html5_video()
1302+
elif fmt == 'jshtml':
1303+
return anim_to_jshtml(self)
9421304

9431305

9441306
class TimedAnimation(Animation):
421 Bytes
Loading
427 Bytes
Loading
324 Bytes
Loading
295 Bytes
Loading
427 Bytes
Loading
336 Bytes
Loading
461 Bytes
Loading

0 commit comments

Comments
 (0)