I’ve developed a new technique for using animations to convey uncertainty. I’m working on publishing a paper, but it won’t be enough by itself—after all, I can’t very well animate paper! So I’m making a companion website to show my animations in action.
One library stood out above the rest: plot.ly.
It’s used as the (semi-?)official plotting library for ggplot2, my favourite graphing library for R, so I knew it would be able to handle any type of plot I could want. It also had a documented animation feature that looked promising. In fact, its documentation was generally very impressive and extensive. (If only I’d read it more carefully at the beginning!)
Testing out plot.ly
I knew plot.ly could produce all the graph styles I needed. The main thing I needed to learn was whether it could animate them. Staying laser-focused on this question told me what I didn’t need.
- I didn’t need to code up my specialized animation technique.
- I didn’t even need my data to have any meaning at all!
- All I did need was a bunch of arbitrary data that changes over time.
I cooked up some vaguely sine-wave-y traces as my data, and had them undergo decaying oscillations as my animation. Overall, I was pleasantly surprised how easy it was to get started. I soon felt satisfied that plot.ly would be able to meet my needs.
The glitch and the clues
There was only one small problem. Every once in a while, the animation would go haywire between frames. I tried to ignore it at first, but soon realized it wasn’t going away. Here’s an example:
It starts out fine, but after a few seconds, we get a keyframe that sweeps out a bit of area from right to left. Then it gets worse and worse—and when the animation restarts, it really goes crazy!
Fortunately, I persisted. I kept turning the problem over in my mind. Before long, I noticed a few clues.
- The glitches only seemed to happen when a line got closer to being straight.
- The glitches were remarkably reproducible—they happened at exactly the same stage of the animation, in exactly the same way, every time.
I realized that this wasn’t some random data corruption somewhere. Instead, it had to be executing a deterministic, reproducible algorithm. This reproucibility—and the targeted, linear motion in the glitches—made me suspect some kind of object constancy bug.
Object constancy crash course
Graph animations are based on keyframes. We start out with one set of data, specify the next set, and ask the plotting library to fill in all the intermediate states.
The goal is to create the illusion that the datapoints are objects that move. To do this, we need to know which datapoints in the first frame are “the same” as the datapoints in the next frame. Associating points in different frames is known as “object constancy”.
It may seem perfectly obvious which points correspond to which, but this is far from true. If we take exactly the same keyframes—i.e., the same \(x\) and \(y\) values—but use different ideas of object constancy, it completely changes the animation!
Here’s an example. We’ll take a simple sinusoidal wave undergoing periodic motion. We’ll also pause for a bit each time we reach a keyframe, simply to make it easier to tell when we reach one. (Note that in a real animation, we wouldn’t pause at the keyframes – as soon as we reach one, we would immediately begin moving to the next!)
First, we’ll say that datapoints are “the same” if they have the same \(x\)-value:
This yields a simple vertical motion for each datapoint. It gives the impression of a rope being waved up and down (albeit rather slowly!).
But this is not the only possible notion of object constancy. Now, let’s say that datapoints are “the same” if they have the same \(y\)-value:
This gives a rather different impression! Instead of a rope being waved up and down, we have a rigid shape moving to the right (except for the final point, which must zip back to the start each time).
Note that the data at the keyframes are identical. (Every time the motion pauses, the graphs look the same.) The only thing that has changed is our notion of which datapoint in the new keyframe is “the same” as each datapoint in the old one.
To drive home this point even further, let’s base our object constancy on a completely random association:
Again: the keyframes are still the same! Every time the motion pauses, we can see that it’s the same shape as the other graphs, and the shape ultimately changes in the same way. But between those keyframes, the datapoints play musical chairs, and the motion is wild and disordered.
This last example is very suggestive: its scrambled motions are reminiscent of the jerky glitches I was seeing. So, I decided to get explicit about object constancy. I gave each datapoint an ID, kept constant from frame to frame. Surely, the plotting library could keep things lined up if I spelled it out?
Nope. Adding IDs made no difference at all to the behaviour. Not only did I still get a glitch, but it was the exact same glitch!
You can’t ID points that don’t exist
The next step wasn’t obvious, but eventually, I did figure it out. It has to do with a feature called “simplification”. Here’s how it works.
Say our plot has 3 points in a row, and call them
C. If they fall close to a straight line, then drawing
A---B---C would be pretty similar to drawing
A-------C: you could cut out
B, and save a point! This is what plot.ly’s
simplify option does: it looks for points that it can remove without changing the shape much. Presumably, it gives a big performance boost if you have a huge number of datapoints.
Oh, and it’s on by default.
Unfortunately, line simplification interacts really badly with animations.
The key insight is this: when you change the data, you change which points are “redundant”. We remove the redundant datapoints from each keyframe individually, but the shape of each keyframe’s data makes different points redundant. Thus, we end up with points popping in and out of existence all the time.
You can’t have object constancy if the objects don’t constantly exist!
So the library does the best it can. For each point on one keyframe, it picks some point on the next keyframe to associate as “the same” point. The result is the glitchy, jerky animation we saw above.
Now that we understand the glitch, all we have to do to fix it is turn off line simplification. It really is that simple: animations require object constancy, so we have to keep all the objects!
The animation below shows what we get when we do that. For easy comparison, I’ve included another copy of the glitchy version, too.
The glitch has completely vanished from the un-simplified version (top animation). What a difference!
So, what’s the lesson here?
Should simplification be off by default? That is far from clear: if the library has many more “big dataset” users than animation users, line simplification could be a reasonable default.
Maybe simplification should get turned off only when we animate? I doubt it: adding special logic for every pair of features could easily make the code much too complex. Besides,
Plotly.animate() only gets called for new frames. If the plot was already simplified for the first frame, when it was created, then we’ll still have a glitch.
Perhaps the lesson is about documentation? I do think they could stand to warn users more prominently than they do. The setting is mentioned in the animation docs, but only in passing—in fact, I didn’t find it until I had already solved the problem! (Thus, by the same token, I could stand to temper my initial enthusiasm, and read the docs more carefully before I start.)
However, I think the main takeaway is simply to appreciate how easy it is for choices that make sense in isolation to interact poorly in a complex system.