Design Tradeoffs for Animation

  1. Multiple bitmap images that can each be shown / hidden in-turn (this is the JSAnimation + Matplotlib approach)
    • Pro: Easy to implement, low browser CPU load during playback.
    • Con: Inefficient memory consumption due to large numbers of base64-encoded images.
  2. SMIL
    • Pro: This is potentially the most compact representation, and requires no additional scripting code.
    • Con: Provides the least control - in particular, it seems to preclude playing the animation backwards, scrubbing, etc. The only playback options would be to stop the animation or play it from the beginning.
  3. One SVG containing multiple "frames" that can each be shown / hidden in-turn.
    • Pro: Easy to implement.
    • Con: Inefficient memory consumption due to large quantities of redundant SVG markup. High browser CPU load during playback.
  4. One SVG plus JavaScript code to modify the DOM
    • Pro: Compact representation that only encodes the difference between frames. Low CPU load during playback, so long as the delta is low.
    • Con: Complicates the implementation.

tplot Behavior

After testing all four options, I opted to use the last to embed the plot with animation directly in the notebook. Note that this takes ~200KB, where the multiple-image version from JSAnimation + Matplotlib takes roughly ~40MB:

In [1]:
import numpy
import tplot.color

palette = tplot.color.brewer("Set3")

# Generate some fake data
numpy.random.seed(1234)
x = numpy.random.random(500)
y = numpy.random.random(len(x))
marker = numpy.random.choice(["s", "o", "^"], len(x))
color = numpy.array([palette.color(i) for i in range(len(x))])
sort_order = numpy.argsort(marker)

# Create the plot
canvas = tplot.canvas()
axes = canvas.axes(xlabel="f0", ylabel="f1")
scatterplot = axes.scatterplot(x[sort_order], y[sort_order], size=40, marker=marker[sort_order], color=color[sort_order], opacity=0.2, style={"stroke":"none", "stroke-width":0.4})
label = canvas.text(300, 20, "Frame 0", style={"text-anchor":"middle", "font-weight":"bold"})
canvas.legend(
    ("Triangles", "^", {"fill":"none", "stroke":"black"}),
    ("Circles", "o", {"fill":"none", "stroke":"black"}),
    ("Rectangles", "s", {"fill":"none", "stroke":"black"}),
    position=(450, 100, 100, 70),
    )

def animation(state):
    state.set_text(label, "Frame %s" % state.frame())
    if state.frame() == 0:
        for i in range(len(x)):
            state.set_item_style(scatterplot, i, {"opacity" : 0.2, "stroke" : "none"})
    else:
        state.set_item_style(scatterplot, state.frame()-1, {"opacity" : 1.0, "stroke" : "black"})
canvas.animate(frames=len(x) + 1, callback=animation)

canvas
Out[1]:
0.000.250.500.751.000.000.250.500.751.00f0f1Frame 0TrianglesCirclesRectangles

tplot Cairo Backend

A cairo backend can be used to generate bitmap images:

In [2]:
import tplot.cairo
tplot.cairo.png(canvas, "test.png")

The cairo backend can also generate multiple animation frames:

In [3]:
import IPython.display
for frame, png in enumerate(tplot.cairo.png_frames(canvas)):
    IPython.display.clear_output(wait=True)
    print "Writing frame %s" % frame
    with open("test-%s.png" % frame, "w") as file:
        file.write(png.getvalue())
Writing frame 500

And it can be used to generate video (piping the frames to ffmpeg):

In [4]:
def progress(frame):
    IPython.display.clear_output(wait=True)
    print "Writing frame %s" % frame

tplot.cairo.mp4(canvas, "test.mp4", progress=progress)
Writing frame 500