Skip to content

Import JSAnimation into the animation module. (Fixes #4703) #4821

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Sep 1, 2017

Conversation

dopplershift
Copy link
Contributor

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.

**mode_dict))


def anim_to_jshtml(anim, fps=None, embed_frames=True, default_mode=None):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a specific reason why this is not a method on the animation class?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Because the JSAnimation package had it this way
  2. Because it calls a specific backend, and none of the other backends get a method
  3. Because.....

I don't have a strong preference here. Anyone have an opinion?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have any strong preference. I was just comparing it to the HTLM5 method. I realise that is different now because it's not really a new backend. Either is fine with me.

@jenshnielsen
Copy link
Member

Nice. Seems to work great. It should get a whats new entry and I would like to make sure that we probably credit the work of @jakevdp I guess there is no easy way of retaining the original commit history? At the least it should be mentioned clearly in the whats new entry.

It probably also makes sense to get a reference added to the original repository mentioning that this is now part of Matplotlib when we get close to a release with this in.

In the long term I think we should replace the present icons with Fort Awesome http://fortawesome.github.io/Font-Awesome/ which we already use in the nbagg backend and which IPython/Jupyter uses internally but that can be done in a separate PR and we don't need to hold this up on that.

The tests failures are pep8 and what looks like random failures on Python 2.7 and 3.4

@dopplershift
Copy link
Contributor Author

I absolutely want to credit @jakevdp. I briefly tried to turn the JSAnimation history for html_writer.py into a set of patches, but trying to apply them as mods to our animation.py was just too much for me to handle. I'd be happy to change the author on these commits if it seems like the right thing to do...

@jenshnielsen
Copy link
Member

I think you are right that applying the original commits as patches is near impossible. I think the most important is that the comments, documentation and whats new correctly reflects the origin of the module

@dopplershift
Copy link
Contributor Author

I went ahead and used the Font Awesome icons, to avoid having the original icons in the history. It also removed some code and decreased the size of the generated HTML.

@jenshnielsen
Copy link
Member

Awesome :) I think this is ready to go but I will leave it a little bit to see if anyone else have comments.

@tacaswell
Copy link
Member

I suspect that we are going to have be blacklist the animation module from the pep8 tests (unless you want to linewrap html), figure out how make it skip block quotes, or have a data file with the html blob.

@WeatherGod
Copy link
Member

can't you do # noqc on the statement in question?

On Fri, Jul 31, 2015 at 9:09 AM, Thomas A Caswell notifications@github.com
wrote:

I suspect that we are going to have be blacklist the animation module from
the pep8 tests (unless you want to linewrap html), figure out how make it
skip block quotes, or have a data file with the html blob.


Reply to this email directly or view it on GitHub
#4821 (comment)
.

@dopplershift
Copy link
Contributor Author

I was planning to go ahead and wrap it when I get a chance today.

I don't want anyone to later pep8 my file :)

@jakevdp
Copy link
Contributor

jakevdp commented Jul 31, 2015

The reason I never submitted this to mpl is that it's a pretty hackish solution to the goal: it embeds raw png binary data as base64 strings directly in the resulting HTML string. This works OK for animations consisting of a few frames, but as soon as you try to do something marginally bigger the file size blows up. I've had reports from people who crashed their IPython session by trying to make a simple animation.

With these limitations in mind, I deliberately chose not to put JSAnimation on PyPI, and not to actually cut a code release, because I didn't feel like dealing with the inevitable flow of issue reports from users who don't understand the quirks of the code. That said, I'm fine with this being in matplotlib, but do keep in mind what it would mean to maintain and support this kind of partial & limited solution.

@tacaswell tacaswell added this to the proposed next point release milestone Jul 31, 2015
@dopplershift
Copy link
Contributor Author

@jakevdp I think the HTML5 video solution I just added fills the need for larger animations. I do think the JSAnimation support is useful due to the better controls you can get--provided you don't have too many frames. (These kinds of issues are why it defaults to off.)

@dopplershift dopplershift force-pushed the add-jsanimation branch 2 times, most recently from 371e7bb to 0657c02 Compare August 1, 2015 03:14
@efiring
Copy link
Member

efiring commented Aug 1, 2015 via email

@dopplershift
Copy link
Contributor Author

I'm willing to document, but the problem is no different than creating too many image objects in a Python session with the standalone animation support. At some point users who do too much will run into problems--I can bring the notebook to a standstill with a print statement and a stupid loop.

I have an animation with 360 images open using JSAnimation right now, with no problems whatsoever--so we're not exactly talking about something really delicate. My opinion is that we will continue to have far more problems reliably running ffmpeg and mencoder across all platforms than displaying PNG images using javascript in the browser.

@jakevdp
Copy link
Contributor

jakevdp commented Aug 1, 2015

the problem is no different than creating too many image objects in a Python session with the standalone animation support.

The difference is that the HTML output can be persistent.

Say someone is using IPython and accidentally creates too many image objects in a conventional animation, bogging down their kernel. They can restart the kernel and try again. No harm done.

Say, however, that someone is using the notebook and accidentally embeds gigabytes of pngs into the HTML output, crashing their browser. The only way to recover the notebook is to somehow strip that output, but you can't do that via any of the IPython tools that try to first open the notebook. Unless the user is comfortable editing JSON by hand to remove the offending lines (and has a program that can handle editing a very large file), the notebook is basically lost.

I'm not saying that JSAnimation shouldn't be added to mpl; I'm just saying it's a hacked solution and we should be well aware of that before committing it.

@dopplershift
Copy link
Contributor Author

How about adding a limit to size of embedded (base64 encoded) content? This could be another rcparam, working for both this and the html5 video stuff. Default to....250 MB?

@jakevdp
Copy link
Contributor

jakevdp commented Aug 1, 2015

How about adding a limit to size of embedded (base64 encoded) content?

I like that! Rather than an rcparam, though, it could be an argument to the converter. Then if the user tries something too big. Maybe set it purposely low, like ~10MB, and have a very informative early error message (maybe based on the size of the first frame * number of frames) then the user can explicitly allow a huge animation if that's what they want.

@dopplershift
Copy link
Contributor Author

My only problem with no rcparam is how to set it for the IPython display hook.

@WeatherGod
Copy link
Member

Anything holding up this PR? This PR happens to fix a seg-fault for me as mentioned in #4967.

@tacaswell
Copy link
Member

@jakevdp 's concern (which I am not sure if @dopplershift addressed well enough) and understanding why this fixes the segfault.

@dopplershift
Copy link
Contributor Author

There's the question of whether it should be an rcparam or an explicit argument to the converter (I implemented the former), but otherwise the concerns are addressed IMO.

options=' '.join(options))
return VIDEO_TAG.format(video=self._base64_video,
size=self._video_size,
options=' '.join(options))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be indented, right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ping. I also think this was not intentional?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it was indented so that we only return the tag if the base64-encoded video data was created (i.e. not too large). The mistake was not to have an alternative return if things go wrong. Will fix.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not clear if this has been addressed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's an else right below this comment now. :)

@jakevdp
Copy link
Contributor

jakevdp commented Sep 1, 2015

Looks pretty good – I noted a few things. Have you run tests on this branch? That indented code should have broken some things.

Also, there is a package that has forked JSAnimation and had to add some script to fix filenames which had spaces in them. I can't say I fully understand the source of the error, but this PR has their fix to the issue.

@tacaswell
Copy link
Member

@dopplershift I broke this by cherry picking one of your commits into #4995

@tacaswell
Copy link
Member

attn @choldgraf @NelleV This is probably the right representation to try to grab to put into sphinx-gallery output.

@tacaswell
Copy link
Member

Can the js and template code be moved to a private module (just to keep animation.py a bit closer to under control)?

@dopplershift
Copy link
Contributor Author

Done.

def new_id(cls):
#return '%16x' % cls.rng.getrandbits(64)
return ''.join(cls.rng.choice(string.ascii_uppercase)
for x in range(16))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just use https://docs.python.org/3.6/library/uuid.html (whichever version you like) and drop rng? (at least I don't think .rng should be exposed)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, done.

# Can't open a second time while opened on windows. So we avoid
# deleting when closed, and delete manually later.
with tempfile.NamedTemporaryFile(suffix='.html', delete=False) as f:
anim.save(f.name, writer=HTMLWriter(fps=fps,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we just write to a BytesIO? (when using a subprocess to encode this would be tricky due to the need to use pipes but here it should be straightforward, no?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd love to, and it makes sense, but I think that's a more invasive a change to the original code (which I didn't write) than I'm comfortable with. Animation itself currently assumes a string filename since it relies heavily on command line tools for writing.

Copy link
Contributor

@anntzer anntzer Aug 31, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, in order to make things work on Windows more easily (I have to deal with that all the time :-() you can create a TemporaryDirectory and use whatever filename you want in it -- then you still get proper cleanup at contextmanager exit. Although that probably means depending on https://github.com/pjdelport/backports.tempfile on AntiquePython (or https://github.com/minrk/backports.temporarydirectory but that doesn't appear to be on PyPI)

TBH I still prefer the BytesIO option, but heh.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It just requires adding an in-memory option alongside the existing file option, and using that for the jshtml converter. Just a little late in the game to be adding that, especially given that I didn't write the HTMLWriter.

if vid_len >= embed_limit:
import warnings
warnings.warn('Animation movie is {0} bytes, exceeding '
'the limit of {1}. If you\'re sure you want a '
Copy link
Contributor

@anntzer anntzer Aug 31, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use double quotes around and you won't need to escape single quotes.
No need to number the "{}"'s (here and throughout the PR).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


def _repr_html_(self):
'''IPython display hook for rendering.'''
fmt = rcParams['animation.html']
if fmt == 'html5':
return self.to_html5_video()
elif fmt == 'jshtml':
return anim_to_jshtml(self)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any reason why anim_to_jshtml is a freestanding function and not a method like to_html5_video? (in which case it should be to_jshtml)
(FTR I don't have anything against freestanding functions, it's more an API consistency thing)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think because this started as an external code, so the function made sense. I've changed to make things more consistent.

@@ -0,0 +1,210 @@
# Javascript template for HTMLWriter
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps _animation_data.py? (like _color_data.py)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like it.

@dopplershift
Copy link
Contributor Author

I think I resolved @anntzer 's comments.

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.
@dopplershift
Copy link
Contributor Author

Grrr. Fixed pep8 line length issues.

@dopplershift
Copy link
Contributor Author

Not sure if I want to kick AppVeyor again or not, there seems to be some sporadic problem while it runs test_artists. Last time it failed during 2nd job, this time it was during the 3rd job.

imgdata64 = encodebytes(f.read()).decode('ascii')
self._total_bytes += len(imgdata64)
if self._total_bytes >= self._bytes_limit:
import warnings
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move the import to top?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heh, it's already there. Removed a few instances of the import lower down.

reflect_checked='')
mode_dict[self.default_mode + '_checked'] = 'checked'

interval = int(1000. / self.fps)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1000, no dot (in a py3 world "/" definitely means float div :-))

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tends to happen in 2 year old PRs... 😉

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually should probably just use // since it's casting to int anyway.

import warnings
warnings.warn("unrecognized default_mode: using 'loop'")

self._saved_frames = list()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"= []"

"""Generate HTML representation of the animation"""
if fps is None and hasattr(self, '_interval'):
# Convert interval in ms to frames per second
fps = 1000. / self._interval
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"1000"

self.fig.savefig(f, format=self.frame_format,
dpi=self.dpi, **savefig_kwargs)
f.seek(0)
imgdata64 = encodebytes(f.read()).decode('ascii')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

f.getvalue(), then you don't need to seek first
shouldn't you just use bytes (and open the final file in binary mode) throughout?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Too much template processing with format to use bytes. Then I'd just have to be using encode everywhere, which doesn't feel like a win.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Used getvalue.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The argument regarding format sounds reasonable.

dopplershift and others added 4 commits August 31, 2017 18:17
This makes both the HTMLWriter and the HTML5 video support check the
amount of data being saved into HTML against the rc parameter
"animation.embed_limit".
@dopplershift
Copy link
Contributor Author

Alright, one more time.

@tacaswell tacaswell merged commit e5b9f7d into matplotlib:master Sep 1, 2017
@dopplershift dopplershift deleted the add-jsanimation branch September 1, 2017 03:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Release critical For bugs that make the library unusable (segfaults, incorrect plots, etc) and major regressions. topic: animation
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants