|
37 | 37 | import abc
|
38 | 38 | import contextlib
|
39 | 39 | import tempfile
|
| 40 | +import uuid |
40 | 41 | import warnings
|
| 42 | +from matplotlib._animation_data import (DISPLAY_TEMPLATE, INCLUDED_FRAMES, |
| 43 | + JS_INCLUDE) |
41 | 44 | from matplotlib.cbook import iterable, deprecated
|
42 | 45 | from matplotlib.compat import subprocess
|
43 | 46 | from matplotlib import verbose
|
44 | 47 | 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 |
45 | 52 |
|
46 | 53 | # Process creation flag for subprocess to prevent it raising a terminal
|
47 | 54 | # window. See for example:
|
@@ -876,6 +883,112 @@ def _args(self):
|
876 | 883 | + self.output_args)
|
877 | 884 |
|
878 | 885 |
|
| 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 | + |
879 | 992 | class Animation(object):
|
880 | 993 | '''This class wraps the creation of an animation using matplotlib.
|
881 | 994 |
|
@@ -1288,11 +1401,44 @@ def to_html5_video(self):
|
1288 | 1401 | size=self._video_size,
|
1289 | 1402 | options=' '.join(options))
|
1290 | 1403 |
|
| 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 | + |
1291 | 1435 | def _repr_html_(self):
|
1292 | 1436 | '''IPython display hook for rendering.'''
|
1293 | 1437 | fmt = rcParams['animation.html']
|
1294 | 1438 | if fmt == 'html5':
|
1295 | 1439 | return self.to_html5_video()
|
| 1440 | + elif fmt == 'jshtml': |
| 1441 | + return self.to_jshtml() |
1296 | 1442 |
|
1297 | 1443 |
|
1298 | 1444 | class TimedAnimation(Animation):
|
|
0 commit comments