Skip to content

Commit 34b6ea3

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 4f73376 commit 34b6ea3

File tree

4 files changed

+362
-2
lines changed

4 files changed

+362
-2
lines changed

lib/matplotlib/_animation_data.py

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
# Javascript template for HTMLWriter
2+
JS_INCLUDE = """
3+
<link rel="stylesheet"
4+
href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/
5+
css/font-awesome.min.css">
6+
<script language="javascript">
7+
/* Define the Animation class */
8+
function Animation(frames, img_id, slider_id, interval, loop_select_id){
9+
this.img_id = img_id;
10+
this.slider_id = slider_id;
11+
this.loop_select_id = loop_select_id;
12+
this.interval = interval;
13+
this.current_frame = 0;
14+
this.direction = 0;
15+
this.timer = null;
16+
this.frames = new Array(frames.length);
17+
18+
for (var i=0; i<frames.length; i++)
19+
{
20+
this.frames[i] = new Image();
21+
this.frames[i].src = frames[i];
22+
}
23+
document.getElementById(this.slider_id).max = this.frames.length - 1;
24+
this.set_frame(this.current_frame);
25+
}
26+
27+
Animation.prototype.get_loop_state = function(){
28+
var button_group = document[this.loop_select_id].state;
29+
for (var i = 0; i < button_group.length; i++) {
30+
var button = button_group[i];
31+
if (button.checked) {
32+
return button.value;
33+
}
34+
}
35+
return undefined;
36+
}
37+
38+
Animation.prototype.set_frame = function(frame){
39+
this.current_frame = frame;
40+
document.getElementById(this.img_id).src =
41+
this.frames[this.current_frame].src;
42+
document.getElementById(this.slider_id).value = this.current_frame;
43+
}
44+
45+
Animation.prototype.next_frame = function()
46+
{
47+
this.set_frame(Math.min(this.frames.length - 1, this.current_frame + 1));
48+
}
49+
50+
Animation.prototype.previous_frame = function()
51+
{
52+
this.set_frame(Math.max(0, this.current_frame - 1));
53+
}
54+
55+
Animation.prototype.first_frame = function()
56+
{
57+
this.set_frame(0);
58+
}
59+
60+
Animation.prototype.last_frame = function()
61+
{
62+
this.set_frame(this.frames.length - 1);
63+
}
64+
65+
Animation.prototype.slower = function()
66+
{
67+
this.interval /= 0.7;
68+
if(this.direction > 0){this.play_animation();}
69+
else if(this.direction < 0){this.reverse_animation();}
70+
}
71+
72+
Animation.prototype.faster = function()
73+
{
74+
this.interval *= 0.7;
75+
if(this.direction > 0){this.play_animation();}
76+
else if(this.direction < 0){this.reverse_animation();}
77+
}
78+
79+
Animation.prototype.anim_step_forward = function()
80+
{
81+
this.current_frame += 1;
82+
if(this.current_frame < this.frames.length){
83+
this.set_frame(this.current_frame);
84+
}else{
85+
var loop_state = this.get_loop_state();
86+
if(loop_state == "loop"){
87+
this.first_frame();
88+
}else if(loop_state == "reflect"){
89+
this.last_frame();
90+
this.reverse_animation();
91+
}else{
92+
this.pause_animation();
93+
this.last_frame();
94+
}
95+
}
96+
}
97+
98+
Animation.prototype.anim_step_reverse = function()
99+
{
100+
this.current_frame -= 1;
101+
if(this.current_frame >= 0){
102+
this.set_frame(this.current_frame);
103+
}else{
104+
var loop_state = this.get_loop_state();
105+
if(loop_state == "loop"){
106+
this.last_frame();
107+
}else if(loop_state == "reflect"){
108+
this.first_frame();
109+
this.play_animation();
110+
}else{
111+
this.pause_animation();
112+
this.first_frame();
113+
}
114+
}
115+
}
116+
117+
Animation.prototype.pause_animation = function()
118+
{
119+
this.direction = 0;
120+
if (this.timer){
121+
clearInterval(this.timer);
122+
this.timer = null;
123+
}
124+
}
125+
126+
Animation.prototype.play_animation = function()
127+
{
128+
this.pause_animation();
129+
this.direction = 1;
130+
var t = this;
131+
if (!this.timer) this.timer = setInterval(function() {
132+
t.anim_step_forward();
133+
}, this.interval);
134+
}
135+
136+
Animation.prototype.reverse_animation = function()
137+
{
138+
this.pause_animation();
139+
this.direction = -1;
140+
var t = this;
141+
if (!this.timer) this.timer = setInterval(function() {
142+
t.anim_step_reverse();
143+
}, this.interval);
144+
}
145+
</script>
146+
"""
147+
148+
149+
# HTML template for HTMLWriter
150+
DISPLAY_TEMPLATE = """
151+
<div class="animation" align="center">
152+
<img id="_anim_img{id}">
153+
<br>
154+
<input id="_anim_slider{id}" type="range" style="width:350px"
155+
name="points" min="0" max="1" step="1" value="0"
156+
onchange="anim{id}.set_frame(parseInt(this.value));"></input>
157+
<br>
158+
<button onclick="anim{id}.slower()"><i class="fa fa-minus"></i></button>
159+
<button onclick="anim{id}.first_frame()"><i class="fa fa-fast-backward">
160+
</i></button>
161+
<button onclick="anim{id}.previous_frame()">
162+
<i class="fa fa-step-backward"></i></button>
163+
<button onclick="anim{id}.reverse_animation()">
164+
<i class="fa fa-play fa-flip-horizontal"></i></button>
165+
<button onclick="anim{id}.pause_animation()"><i class="fa fa-pause">
166+
</i></button>
167+
<button onclick="anim{id}.play_animation()"><i class="fa fa-play"></i>
168+
</button>
169+
<button onclick="anim{id}.next_frame()"><i class="fa fa-step-forward">
170+
</i></button>
171+
<button onclick="anim{id}.last_frame()"><i class="fa fa-fast-forward">
172+
</i></button>
173+
<button onclick="anim{id}.faster()"><i class="fa fa-plus"></i></button>
174+
<form action="#n" name="_anim_loop_select{id}" class="anim_control">
175+
<input type="radio" name="state"
176+
value="once" {once_checked}> Once </input>
177+
<input type="radio" name="state"
178+
value="loop" {loop_checked}> Loop </input>
179+
<input type="radio" name="state"
180+
value="reflect" {reflect_checked}> Reflect </input>
181+
</form>
182+
</div>
183+
184+
185+
<script language="javascript">
186+
/* Instantiate the Animation class. */
187+
/* The IDs given should match those used in the template above. */
188+
(function() {{
189+
var img_id = "_anim_img{id}";
190+
var slider_id = "_anim_slider{id}";
191+
var loop_select_id = "_anim_loop_select{id}";
192+
var frames = new Array({Nframes});
193+
{fill_frames}
194+
195+
/* set a timeout to make sure all the above elements are created before
196+
the object is initialized. */
197+
setTimeout(function() {{
198+
anim{id} = new Animation(frames, img_id, slider_id, {interval},
199+
loop_select_id);
200+
}}, 0);
201+
}})()
202+
</script>
203+
"""
204+
205+
INCLUDED_FRAMES = """
206+
for (var i=0; i<{Nframes}; i++){{
207+
frames[i] = "{frame_dir}/frame" + ("0000000" + i).slice(-7) +
208+
".{frame_format}";
209+
}}
210+
"""

lib/matplotlib/animation.py

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,18 @@
3737
import abc
3838
import contextlib
3939
import tempfile
40+
import uuid
4041
import warnings
42+
from matplotlib._animation_data import (DISPLAY_TEMPLATE, INCLUDED_FRAMES,
43+
JS_INCLUDE)
4144
from matplotlib.cbook import iterable, deprecated
4245
from matplotlib.compat import subprocess
4346
from matplotlib import verbose
4447
from matplotlib import rcParams, rcParamsDefault, rc_context
48+
if sys.version_info < (3, 0):
49+
from cStringIO import StringIO as InMemory
50+
else:
51+
from io import BytesIO as InMemory
4552

4653
# Process creation flag for subprocess to prevent it raising a terminal
4754
# window. See for example:
@@ -876,6 +883,112 @@ def _args(self):
876883
+ self.output_args)
877884

878885

886+
# Taken directly from jakevdp's JSAnimation package at
887+
# http://github.com/jakevdp/JSAnimation
888+
def _included_frames(frame_list, frame_format):
889+
"""frame_list should be a list of filenames"""
890+
return INCLUDED_FRAMES.format(Nframes=len(frame_list),
891+
frame_dir=os.path.dirname(frame_list[0]),
892+
frame_format=frame_format)
893+
894+
895+
def _embedded_frames(frame_list, frame_format):
896+
"""frame_list should be a list of base64-encoded png files"""
897+
template = ' frames[{0}] = "data:image/{1};base64,{2}"\n'
898+
embedded = "\n"
899+
for i, frame_data in enumerate(frame_list):
900+
embedded += template.format(i, frame_format,
901+
frame_data.replace('\n', '\\\n'))
902+
return embedded
903+
904+
905+
@writers.register('html')
906+
class HTMLWriter(FileMovieWriter):
907+
supported_formats = ['png', 'jpeg', 'tiff', 'svg']
908+
args_key = 'animation.html_args'
909+
910+
@classmethod
911+
def isAvailable(cls):
912+
return True
913+
914+
def __init__(self, fps=30, codec=None, bitrate=None, extra_args=None,
915+
metadata=None, embed_frames=False, default_mode='loop'):
916+
self.embed_frames = embed_frames
917+
self.default_mode = default_mode.lower()
918+
919+
if self.default_mode not in ['loop', 'once', 'reflect']:
920+
self.default_mode = 'loop'
921+
import warnings
922+
warnings.warn("unrecognized default_mode: using 'loop'")
923+
924+
self._saved_frames = list()
925+
super(HTMLWriter, self).__init__(fps, codec, bitrate,
926+
extra_args, metadata)
927+
928+
def setup(self, fig, outfile, dpi, frame_dir=None):
929+
if os.path.splitext(outfile)[-1] not in ['.html', '.htm']:
930+
raise ValueError("outfile must be *.htm or *.html")
931+
932+
if not self.embed_frames:
933+
if frame_dir is None:
934+
frame_dir = outfile.rstrip('.html') + '_frames'
935+
if not os.path.exists(frame_dir):
936+
os.makedirs(frame_dir)
937+
frame_prefix = os.path.join(frame_dir, 'frame')
938+
else:
939+
frame_prefix = None
940+
941+
super(HTMLWriter, self).setup(fig, outfile, dpi,
942+
frame_prefix, clear_temp=False)
943+
944+
def grab_frame(self, **savefig_kwargs):
945+
if self.embed_frames:
946+
suffix = '.' + self.frame_format
947+
f = InMemory()
948+
self.fig.savefig(f, format=self.frame_format,
949+
dpi=self.dpi, **savefig_kwargs)
950+
f.seek(0)
951+
imgdata64 = encodebytes(f.read()).decode('ascii')
952+
self._saved_frames.append(imgdata64)
953+
else:
954+
return super(HTMLWriter, self).grab_frame(**savefig_kwargs)
955+
956+
def _run(self):
957+
# make a duck-typed subprocess stand in
958+
# this is called by the MovieWriter base class, but not used here.
959+
class ProcessStandin(object):
960+
returncode = 0
961+
962+
def communicate(self):
963+
return '', ''
964+
965+
self._proc = ProcessStandin()
966+
967+
# save the frames to an html file
968+
if self.embed_frames:
969+
fill_frames = _embedded_frames(self._saved_frames,
970+
self.frame_format)
971+
else:
972+
# temp names is filled by FileMovieWriter
973+
fill_frames = _included_frames(self._temp_names,
974+
self.frame_format)
975+
976+
mode_dict = dict(once_checked='',
977+
loop_checked='',
978+
reflect_checked='')
979+
mode_dict[self.default_mode + '_checked'] = 'checked'
980+
981+
interval = int(1000. / self.fps)
982+
983+
with open(self.outfile, 'w') as of:
984+
of.write(JS_INCLUDE)
985+
of.write(DISPLAY_TEMPLATE.format(id=uuid.uuid4().hex,
986+
Nframes=len(self._temp_names),
987+
fill_frames=fill_frames,
988+
interval=interval,
989+
**mode_dict))
990+
991+
879992
class Animation(object):
880993
'''This class wraps the creation of an animation using matplotlib.
881994
@@ -1288,11 +1401,44 @@ def to_html5_video(self):
12881401
size=self._video_size,
12891402
options=' '.join(options))
12901403

1404+
def to_jshtml(self, fps=None, embed_frames=True, default_mode=None):
1405+
"""Generate HTML representation of the animation"""
1406+
if fps is None and hasattr(self, '_interval'):
1407+
# Convert interval in ms to frames per second
1408+
fps = 1000. / self._interval
1409+
1410+
# If we're not given a default mode, choose one base on the value of
1411+
# the repeat attribute
1412+
if default_mode is None:
1413+
default_mode = 'loop' if self.repeat else 'once'
1414+
1415+
if hasattr(self, "_html_representation"):
1416+
return self._html_representation
1417+
else:
1418+
# Can't open a second time while opened on windows. So we avoid
1419+
# deleting when closed, and delete manually later.
1420+
with tempfile.NamedTemporaryFile(suffix='.html',
1421+
delete=False) as f:
1422+
self.save(f.name, writer=HTMLWriter(fps=fps,
1423+
embed_frames=embed_frames,
1424+
default_mode=default_mode))
1425+
# Re-open and get content
1426+
with open(f.name) as fobj:
1427+
html = fobj.read()
1428+
1429+
# Now we can delete
1430+
os.remove(f.name)
1431+
1432+
self._html_representation = html
1433+
return html
1434+
12911435
def _repr_html_(self):
12921436
'''IPython display hook for rendering.'''
12931437
fmt = rcParams['animation.html']
12941438
if fmt == 'html5':
12951439
return self.to_html5_video()
1440+
elif fmt == 'jshtml':
1441+
return self.to_jshtml()
12961442

12971443

12981444
class TimedAnimation(Animation):

0 commit comments

Comments
 (0)