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

Skip to content

Commit 52c7b6d

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 52c7b6d

File tree

3 files changed

+370
-4
lines changed

3 files changed

+370
-4
lines changed

lib/matplotlib/animation.py

Lines changed: 363 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,17 @@
2929
import itertools
3030
import base64
3131
import contextlib
32+
import random
33+
import string
3234
import tempfile
3335
from matplotlib.cbook import iterable, is_string_like
3436
from matplotlib.compat import subprocess
3537
from matplotlib import verbose
3638
from matplotlib import rcParams
39+
if sys.version_info < (3, 0):
40+
from cStringIO import StringIO as InMemory
41+
else:
42+
from io import BytesIO as InMemory
3743

3844
# Process creation flag for subprocess to prevent it raising a terminal
3945
# window. See for example:
@@ -561,6 +567,361 @@ def _args(self):
561567
+ self.output_args)
562568

563569

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

9431306

9441307
class TimedAnimation(Animation):

0 commit comments

Comments
 (0)