8000 Import JSAnimation into the animation module. · matplotlib/matplotlib@74e6175 · GitHub
[go: up one dir, main page]

Skip to content

Commit 74e6175

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 74e6175

File tree

3 files changed

+349
-4
lines changed

3 files changed

+349
-4
lines changed

lib/matplotlib/animation.py

Lines changed: 342 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,340 @@ def _args(self):
561567
+ self.output_args)
562568

563569

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

9431285

9441286
class TimedAnimation(Animation):

0 commit comments

Comments
 (0)
0