<jupyter_start><jupyter_text>Animation Toyplot can also create animated figures, by recording changes to a figure over time.  Assume you've setup the following scatterplot:<jupyter_code>import numpy
x = numpy.random.normal(size=100)
y = numpy.random.normal(size=len(x))
import toyplot
canvas = toyplot.Canvas(300, 300)
axes = canvas.cartesian()
mark = axes.scatterplot(x, y, size=10)<jupyter_output><empty_output><jupyter_text>Suppose we want to show the order in which the samples were drawn from some distribution.  We could use the `fill` parameter to map each sample's index to a color, but an animation can be more intuitive.  We can use  :meth:`toyplot.canvas.Canvas.animate` to add a sequence of animation frames to the canvas.  We pass the number of frames and a callback function as arguments, and the callback function will be called once per frame with a single :class:`frame ` argument.  The callback uses the frame object to retrieve information about the frame and record any changes that should be made to the canvas at that frame.  In the example below, we set the opacity of each scatterplot datum to 5% in the first frame, then change them back to 100% over the course of the animation:<jupyter_code>canvas = toyplot.Canvas(300, 300)
axes = canvas.cartesian()
mark = axes.scatterplot(x, y, size=10)

for frame in canvas.frames(len(x) + 1):
    if frame.number == 0:
        for i in range(len(x)):
            frame.set_datum_style(mark, 0, i, style={"opacity":0.1})
    else:
        frame.set_datum_style(mark, 0, frame.number - 1, style={"opacity":1.0})<jupyter_output><empty_output><jupyter_text>Let's try animating something other than a datum style - in the following example, we add a text mark to the canvas, and use it to display information about the frame:<jupyter_code>canvas = toyplot.Canvas(300, 300)
axes = canvas.cartesian()
mark = axes.scatterplot(x, y, size=10)
text = canvas.text(150, 20, " ")

for frame in canvas.frames(len(x) + 1):
    label = "%s/%s <small>(%.2f s)</small>" % (frame.number + 1, frame.count, frame.begin)
    frame.set_datum_text(text, 0, 0, label)
    
    if frame.number == 0:
        for i in range(len(x)):
            frame.set_datum_style(mark, 0, i, style={"opacity":0.05})
    else:
        frame.set_datum_style(mark, 0, frame.number - 1, style={"opacity":1.0})<jupyter_output><empty_output><jupyter_text>Note from this example that each frame has a zero-based frame index, along with begin and end times, which are measured in seconds.  If you look closely, you'll see that the difference in begin and end times is 0.03 seconds for each frame, which corresponds to a default 30 frames per second.  If we want to control the framerate, we can pass a (frames, framerate) tuple when we call :meth:`toyplot.canvas.Canvas.animate` (note that the playback is slower, and the times for the frames are changed):<jupyter_code>canvas = toyplot.Canvas(300, 300)
axes = canvas.cartesian()
mark = axes.scatterplot(x, y, size=10)
text = canvas.text(150, 20, " ")

for frame in canvas.frames((len(x) + 1, 5)):
    label = "%s/%s <small>(%.2f s)</small>" % (frame.number + 1, frame.count, frame.begin)
    frame.set_datum_text(text, 0, 0, label)

    if frame.number == 0:
        for i in range(len(x)):
            frame.set_datum_style(mark, 0, i, style={"opacity":0.05})
    else:
        frame.set_datum_style(mark, 0, frame.number - 1, style={"opacity":1.0})<jupyter_output><empty_output><jupyter_text>Sometimes the callback approach to animation is awkward, particularly if you simply have a one-time "event" that needs to happen in the middle of the animation.  In this case, you can use :meth:`toyplot.canvas.Canvas.time` to record changes for individual frames:<jupyter_code>canvas = toyplot.Canvas(300, 300)
axes = canvas.cartesian()
mark = axes.scatterplot(x, y, size=10)
text = canvas.text(150, 20, " ")
text2 = canvas.text(150, 35, " ")

for frame in canvas.frames(len(x) + 1):
    label = "%s/%s <small>(%.2f s)</small>" % (frame.number + 1, frame.count, frame.begin)
    frame.set_datum_text(text, 0, 0, label)

    if frame.number == 0:
        for i in range(len(x)):
            frame.set_datum_style(mark, 0, i, style={"opacity":0.05})
    else:
        frame.set_datum_style(mark, 0, frame.number - 1, style={"opacity":1.0})

canvas.frame(0.0).set_datum_text(text2, 0, 0, "Halfway There!", style={"font-weight":"bold", "opacity":0.2})
canvas.frame(50.0 * (1.0 / 30.0)).set_datum_text(text2, 0, 0, "Halfway There!", style={"font-weight":"bold", "fill":"blue"})<jupyter_output><empty_output><jupyter_text>Note that when you combine :meth:`toyplot.canvas.Canvas.animate` and :meth:`toyplot.canvas.Canvas.time`, you don't have to force the "frames" to line-up ... you can record events in any order and at any point in time, whether there are existing frames at those times or not.  In fact, you could call :meth:`toyplot.canvas.Canvas.animate` multiple times, if you wanted to animate events happening at different rates:<jupyter_code>canvas = toyplot.Canvas(100, 100, style={"background-color":"ivory"})
t1 = canvas.text(50, 33, " ")
t2 = canvas.text(50, 66, " ")

for frame in canvas.frames((10, 2)):
    frame.set_datum_text(t1, 0, 0, "1 hz", style={"opacity":frame.number % 2.0})

for frame in canvas.frames((20, 4)):
    frame.set_datum_text(t2, 0, 0, "2 hz", style={"opacity":frame.number % 2.0})
import toyplot.mp4
toyplot.mp4.render(canvas, "test.mp4", progress=print)<jupyter_output>0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{'fraction_non_alphanumeric': 0.12862844702467344, 'fraction_numerical': 0.038098693759071114, 'mean_word_length': 4.3837890625, 'pattern_counts': {'":': 15, '<': 30, '<?xml version=': 0, '>': 30, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '6', 'dir': 'jupyter-structured-clean-dedup', 'id': '413952', 'license': 'BSD-3-Clause', 'n_tokens_mistral': 1982, 'n_tokens_neox': 1814, 'n_words': 619, 'path': 'notebooks/animation.ipynb', 'repo_name': 'eaton-lab/toyplot', 'size': '5512'}